diff --git a/cypress/e2e/Population_statistics.js b/cypress/e2e/Population_statistics.js index 06b3079e38..14e5fdb5ed 100644 --- a/cypress/e2e/Population_statistics.js +++ b/cypress/e2e/Population_statistics.js @@ -109,7 +109,8 @@ describe('Population statistics tests', () => { cy.contains('Courses of class').click() cy.get('[data-cy=curriculum-picker]').contains('2020–2023') cy.get('[data-cy=toggle-group-module-MAT-tyo]') - cy.get('[data-cy=curriculum-picker]').click().contains('2023–2026').click() + cy.get('[data-cy=curriculum-picker]').click() + cy.contains('2023–2026').click() cy.get('[data-cy=toggle-group-module-MAT-tyo]').should('not.exist') }) @@ -117,14 +118,17 @@ describe('Population statistics tests', () => { cy.visit(pathToMathBSc2020) cy.contains('Courses of class').click() cy.intercept('/api/v2/populationstatistics/courses').as('courseData') - cy.get('[data-cy=curriculum-picker]').click().contains('2020–2023').click() + cy.get('[data-cy=curriculum-picker]').click() + cy.contains('2020–2023').click({ force: true }) + cy.wait('@courseData').then(({ response }) => { expect(response.body).to.have.property('allStudents') expect(response.body).to.have.property('coursestatistics') expect(response.body.allStudents).to.equal(27) expect(response.body.coursestatistics.some(stat => stat.course.code === 'DIGI-100')).to.equal(true) }) - cy.get('[data-cy=curriculum-picker]').click().contains('2023–2026').click() + cy.get('[data-cy=curriculum-picker]').click() + cy.contains('2023–2026').click() cy.wait('@courseData').then(({ response }) => { expect(response.body).to.have.property('allStudents') expect(response.body).to.have.property('coursestatistics') diff --git a/cypress/e2e/Students.js b/cypress/e2e/Students.js index 307f6da2cc..509bd70fef 100644 --- a/cypress/e2e/Students.js +++ b/cypress/e2e/Students.js @@ -8,13 +8,21 @@ const student = { email: 'sisutestidata134902@testisisudata.fi', } +const typeToSearch = text => { + cy.get("input[placeholder='Search with a student number or name (surname firstname)']").type(text) +} + const typeStudentNumberAndClick = studentNumber => { - cy.get('.prompt').type(studentNumber) - cy.contains('td a', studentNumber).click() + typeToSearch(studentNumber) + cy.contains('td', studentNumber).click() +} + +const clearSearch = () => { + cy.get("input[placeholder='Search with a student number or name (surname firstname)']").clear() } describe('Students tests', () => { - describe('when using basic user', () => { + describe('When using basic user', () => { beforeEach(() => { cy.init() cy.contains('Students').click() @@ -23,7 +31,7 @@ describe('Students tests', () => { }) it('Students search form is usable', () => { - cy.get('.prompt').type(student.lastname) + typeToSearch(student.lastname) cy.contains('Student number') cy.contains('Started') cy.contains('Credits') @@ -37,20 +45,19 @@ describe('Students tests', () => { }) it('Search term must be at least 4 characters long', () => { - cy.get('.prompt').type(student.lastname.slice(0, 3)) - cy.contains('No search results or search term is not accurate enough') - cy.get('.prompt').type(student.lastname.slice(3)) + typeToSearch(student.lastname.slice(0, 3)) + cy.contains('Search term is not accurate enough') + typeToSearch(student.lastname.slice(3)) cy.get('table tbody tr').should('have.length', 1) }) - it('can search with studentnumber too', () => { - cy.get('.prompt').type(student.studentnumber) + it('Can search with studentnumber too', () => { + typeToSearch(student.studentnumber) cy.contains(student.studentnumber) }) it('Can get student specific page by clicking student', () => { - cy.get('.prompt').type(student.studentnumber) - cy.contains('td a', student.studentnumber).click() + typeStudentNumberAndClick(student.studentnumber) cy.contains('Matemaattisten tieteiden kandiohjelma (01.08.2020–31.07.2027)') cy.contains(student.lastname).should('not.exist') cy.contains(student.firstnames).should('not.exist') @@ -69,7 +76,7 @@ describe('Students tests', () => { it("'Update student' button is not shown", () => { typeStudentNumberAndClick(student.studentnumber) - cy.get('div.ui.fluid.card').within(() => { + cy.get('[data-cy=student-info-card]').within(() => { cy.contains('button', 'Update student').should('not.exist') }) }) @@ -88,7 +95,7 @@ describe('Students tests', () => { .siblings() .last() .within(() => { - cy.get('.level').click() + cy.get('a').click() }) cy.url().should('include', '/coursestatistics') cy.contains('MAT12004 Tilastollinen päättely I') @@ -98,7 +105,7 @@ describe('Students tests', () => { it('Has correct Sisu link', () => { typeStudentNumberAndClick(student.studentnumber) - cy.get('[data-cy=sisulink] > a') + cy.get('[data-cy=sisu-link]') .should('have.attr', 'href') .and('include', `https://sisu.helsinki.fi/tutor/role/staff/student/${student.sis_person_id}/basic/basic-info`) }) @@ -110,7 +117,7 @@ describe('Students tests', () => { 'Matemaattisten tieteiden kandiohjelma', 'Oikeustieteen maisterin koulutusohjelmaOikeusnotaarin koulutusohjelma', ] - cy.get('div.ui.fluid.card').within(() => { + cy.get('[data-cy=student-info-card]').within(() => { cy.contains('Enrollments').click() cy.get('table tbody tr') .should('have.length', 3) @@ -122,18 +129,22 @@ describe('Students tests', () => { }) }) - it('Searching with bad inputs doesnt yield results', () => { - cy.get('.prompt').type('SWAG LITTINEN') + it("Searching with bad inputs doesn't yield results", () => { + typeToSearch('SWAG LITTINEN') cy.contains('Student number').should('not.exist') - - cy.get('.prompt').clear().type('01114') + clearSearch() + typeToSearch('01114') cy.contains('Student number').should('not.exist') }) it('Can jump to population page', () => { typeStudentNumberAndClick(student.studentnumber) - cy.contains('.ui.table', 'Completed').within(() => { - cy.get('i.level.up.alternate.icon').click() + cy.get('[data-cy=study-rights-section]').within(() => { + cy.contains('Matemaattisten tieteiden kandiohjelma') + .parent() + .within(() => { + cy.get('a').click() + }) }) cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021') cy.contains('class size 30 students') @@ -141,33 +152,32 @@ describe('Students tests', () => { it('Grade graph works in all three different modes', () => { typeStudentNumberAndClick(student.studentnumber) - cy.contains('a.item', 'Grade graph').click() - cy.contains('text.highcharts-title', 'Grade plot') - cy.contains('a.item', 'Show group mean').click() - cy.get('.labeled.input').within(() => { - cy.contains('.label', 'Group size') + cy.contains('button', 'Grade graph').click() + cy.contains('button', 'Show group mean').click() + cy.get('[data-cy=group-size-input]').within(() => { + cy.contains('label', 'Group size') cy.get('input').should('have.value', '5') }) cy.contains('.highcharts-container text', "Nov '22") cy.contains('.highcharts-container text', "Jul '22").should('not.exist') - cy.contains('text.highcharts-title', 'Grade plot') - cy.get('.labeled.input').within(() => { - cy.contains('.label', 'Group size') - cy.get('input').clear().type('10') + cy.get('[data-cy=group-size-input]').within(() => { + cy.contains('label', 'Group size') + cy.get('input').clear() + cy.get('input').type('10') cy.get('input').should('have.value', '10') }) cy.contains('.highcharts-container text', "Jul '22") cy.contains('.highcharts-container text', "Nov '22").should('not.exist') - cy.contains('a.item', 'Show semester mean').click() + cy.contains('button', 'Show semester mean').click() cy.contains('.highcharts-container text', "Jan '22") }) describe('Bachelor Honours section', () => { it("Shows 'Qualified for Honours' tag and main modules info when the student is qualified", () => { cy.visit('/students/495976') - cy.contains('.divider h4', 'Bachelor Honours') - cy.contains('.green.tag.label', 'Qualified for Honours') - cy.contains('Main courses and other modules').click() + cy.contains('h2', 'Bachelor Honours') + cy.contains('[data-cy=honours-chip-qualified]', 'Qualified for Honours') + cy.contains('Study modules').click() cy.get('[data-cy=main-modules] tbody tr') .should('have.length', 3) .each(($tr, index) => { @@ -186,35 +196,35 @@ describe('Students tests', () => { it("Shows 'Did not graduate in time' when the student has graduated but not in time", () => { cy.visit('/students/540355') - cy.contains('.divider h4', 'Bachelor Honours') - cy.contains('.red.tag.label', 'Not qualified for Honours') - cy.contains('.red.tag.label', 'Did not graduate in time') + cy.contains('h2', 'Bachelor Honours') + cy.contains('[data-cy=honours-chip-not-qualified]', 'Not qualified for Honours') + cy.contains('[data-cy=honours-chip-error]', 'Did not graduate in time') }) it("Shows 'Module grades too low' when the student has graduated in time but has too low grades", () => { cy.visit('/students/547934') - cy.contains('.divider h4', 'Bachelor Honours') - cy.contains('.red.tag.label', 'Not qualified for Honours') - cy.contains('.red.tag.label', 'Module grades too low') + cy.contains('h2', 'Bachelor Honours') + cy.contains('[data-cy=honours-chip-not-qualified]', 'Not qualified for Honours') + cy.contains('[data-cy=honours-chip-error]', 'Module grades too low') }) it("Shows 'Might need further inspection' when the student has graduated in time but has more than four main modules", () => { cy.visit('/students/478837') - cy.contains('.divider h4', 'Bachelor Honours') - cy.contains('.blue.tag.label', 'Might need further inspection') + cy.contains('h2', 'Bachelor Honours') + cy.contains('[data-cy=honours-chip-inspection]', 'Might need further inspection') }) it("Shows 'Has not graduated' when the student has not graduated", () => { cy.visit(`students/${student.studentnumber}`) - cy.contains('.divider h4', 'Bachelor Honours') - cy.contains('.red.tag.label', 'Not qualified for Honours') - cy.contains('.red.tag.label', 'Has not graduated') + cy.contains('h2', 'Bachelor Honours') + cy.contains('[data-cy=honours-chip-not-qualified]', 'Not qualified for Honours') + cy.contains('[data-cy=honours-chip-error]', 'Has not graduated') }) }) }) // Use admin to see all students - describe('when using admin user', () => { + describe('When using admin user', () => { beforeEach(() => { cy.init('/students', 'admin') }) @@ -226,14 +236,14 @@ describe('Students tests', () => { it("'Update student' button is shown", () => { typeStudentNumberAndClick(student.studentnumber) - cy.get('div.ui.fluid.card').within(() => { + cy.get('[data-cy=student-info-card]').within(() => { cy.contains('button', 'Update student') }) }) it('Bachelor Honours section is not shown for students outside of Faculty of Science', () => { cy.visit('/students/453146') - cy.contains('.divider h4', 'Bachelor Honours').should('not.exist') + cy.contains('h2', 'Bachelor Honours').should('not.exist') }) it('When a study plan is selected, courses included in the study plan are highlighted with a blue background', () => { @@ -241,9 +251,11 @@ describe('Students tests', () => { cy.contains('table tbody tr', 'Kulttuurien tutkimuksen kandiohjelma (01.08.2020–30.06.2023)').within(() => { cy.get('td').eq(0).click() }) - cy.contains('table tbody tr', 'Kandidaatintutkielma (KUKA-LIS222)') - .should('have.attr', 'style') - .and('equal', 'background-color: rgb(232, 244, 255);') + cy.contains('table tbody tr', 'Kandidaatintutkielma (KUKA-LIS222)').should( + 'have.css', + 'background-color', + 'rgb(232, 244, 255)' + ) }) it('When a study plan is selected, the time frame of the credit graph is updated', () => { diff --git a/cypress/e2e/Studyprogramme_overview.js b/cypress/e2e/Studyprogramme_overview.js index 9da151a307..b66aa07d60 100644 --- a/cypress/e2e/Studyprogramme_overview.js +++ b/cypress/e2e/Studyprogramme_overview.js @@ -742,16 +742,23 @@ describe('Study programme overview', () => { cy.contains(studentNumber) } - cy.go('back') + for (const studentNumber of studentNumbers) { + cy.visit(`/students/${studentNumber}`) + cy.contains('Tags') + cy.contains(name) + } + + cy.get('a').contains('Matemaattisten tieteiden kandiohjelma').invoke('removeAttr', 'target').click() + cy.url().should('include', '/study-programme/KH50_001?p_tab=4') + deleteTag(name) }) it('deleting a tag from tag view also removes it from students', () => { cy.contains(name).should('not.exist') for (const studentNumber of studentNumbers) { - cy.contains('Students').click() - cy.get('.prompt').type(studentNumber) - cy.contains('a', studentNumber).click() + cy.visit(`/students/${studentNumber}`) + cy.contains('Tags').should('not.exist') cy.contains(name).should('not.exist') } }) diff --git a/services/backend/src/services/students.ts b/services/backend/src/services/students.ts index bdb1f167ef..7b9706d487 100644 --- a/services/backend/src/services/students.ts +++ b/services/backend/src/services/students.ts @@ -344,6 +344,7 @@ export const bySearchTermAndStudentNumbers = async (searchterm: string, studentN }, } : { [Op.or]: [nameLike(terms), studentnumberLike(terms)] }, + order: [['dateofuniversityenrollment', 'DESC NULLS LAST']], }) ).map(formatSharedStudentData) } diff --git a/services/frontend/src/components/PopulationDetails/CurriculumPicker.jsx b/services/frontend/src/components/PopulationDetails/CurriculumPicker.jsx index b18528535c..a336417090 100644 --- a/services/frontend/src/components/PopulationDetails/CurriculumPicker.jsx +++ b/services/frontend/src/components/PopulationDetails/CurriculumPicker.jsx @@ -1,5 +1,6 @@ +import { FormControl, MenuItem, Select } from '@mui/material' import { useEffect, useState } from 'react' -import { Dropdown, Form, Input, Radio } from 'semantic-ui-react' +import { Form, Input, Radio } from 'semantic-ui-react' import { useGetCurriculumsQuery, useGetCurriculumOptionsQuery } from '@/redux/populationCourses' @@ -45,23 +46,22 @@ export const CurriculumPicker = ({ setCurriculum, programmeCodes, disabled, year if (curriculums.length === 0) return null return ( - setSelectedCurriculum(curriculums.find(curriculum => curriculum.id === value))} - options={curriculums.map(curriculum => ({ - key: curriculum.curriculum_period_ids.toSorted().join(', '), - value: curriculum.id, - text: curriculum.curriculumName, - }))} - style={{ - background: '#e3e3e3', - marginLeft: '10px', - padding: '4px 4px 4px 8px', - }} - value={chosenCurriculum.id} - /> + + + ) } diff --git a/services/frontend/src/components/StudentStatistics/StudentInfoCard/studentInfoCard.css b/services/frontend/src/components/PopulationStudents/StudentTable/ProgressTab/index.css similarity index 80% rename from services/frontend/src/components/StudentStatistics/StudentInfoCard/studentInfoCard.css rename to services/frontend/src/components/PopulationStudents/StudentTable/ProgressTab/index.css index 37ee1c2bd3..6c72e8d02f 100644 --- a/services/frontend/src/components/StudentStatistics/StudentInfoCard/studentInfoCard.css +++ b/services/frontend/src/components/PopulationStudents/StudentTable/ProgressTab/index.css @@ -1,12 +1,3 @@ -.cardHeader { - display: flex !important; - justify-content: space-between !important; -} - -.controlIcon { - cursor: pointer; -} - .enrollment-label-no-margin { border-radius: 0.28rem; box-shadow: diff --git a/services/frontend/src/components/PopulationStudents/StudentTable/ProgressTab/index.jsx b/services/frontend/src/components/PopulationStudents/StudentTable/ProgressTab/index.jsx index 06c5a19823..18953dd39e 100644 --- a/services/frontend/src/components/PopulationStudents/StudentTable/ProgressTab/index.jsx +++ b/services/frontend/src/components/PopulationStudents/StudentTable/ProgressTab/index.jsx @@ -6,7 +6,7 @@ import { Icon, Message, Tab } from 'semantic-ui-react' import { StudentInfoItem } from '@/components/common/StudentInfoItem' import { useLanguage } from '@/components/LanguagePicker/useLanguage' -import '@/components/StudentStatistics/StudentInfoCard/studentInfoCard.css' +import './index.css' import { SortableTable } from '@/components/SortableTable' import { useStudentNameVisibility } from '@/components/StudentNameVisibilityToggle' import { ISO_DATE_FORMAT } from '@/constants/date' diff --git a/services/frontend/src/components/Routes/index.jsx b/services/frontend/src/components/Routes/index.jsx index 9d5a350e07..4d6c483602 100644 --- a/services/frontend/src/components/Routes/index.jsx +++ b/services/frontend/src/components/Routes/index.jsx @@ -10,7 +10,6 @@ import { CustomPopulation } from '@/components/CustomPopulation' import { LanguageCenterView } from '@/components/LanguageCenterView' import { PopulationStatistics } from '@/components/PopulationStatistics' import { SegmentDimmer } from '@/components/SegmentDimmer' -import { StudentStatistics } from '@/components/StudentStatistics' import { StudyGuidanceGroups } from '@/components/StudyGuidanceGroups' import { StudyProgramme } from '@/components/StudyProgramme' import { Teachers } from '@/components/Teachers' @@ -22,11 +21,14 @@ import { CloseToGraduation } from '@/pages/CloseToGraduation' import { Faculties } from '@/pages/Faculties' import { Feedback } from '@/pages/Feedback' import { FrontPage } from '@/pages/FrontPage' +import { Students } from '@/pages/Students' +import { StudentDetails } from '@/pages/Students/StudentDetails' +import { StudentSearch } from '@/pages/Students/StudentSearch' import { University } from '@/pages/University' import { ProtectedRoute } from './ProtectedRoute' const routes = { - students: '/students/:studentNumber?', + students: '/students', courseStatistics: '/coursestatistics', teachers: '/teachers/:teacherid?', users: '/users/:userid?', @@ -88,7 +90,10 @@ export const Routes = () => ( } > - } path={routes.students} /> + } path={routes.students}> + } index /> + } path=":studentNumber" /> + } path={routes.custompopulation} /> diff --git a/services/frontend/src/components/StudentNameVisibilityToggle/index.jsx b/services/frontend/src/components/StudentNameVisibilityToggle/index.tsx similarity index 54% rename from services/frontend/src/components/StudentNameVisibilityToggle/index.jsx rename to services/frontend/src/components/StudentNameVisibilityToggle/index.tsx index e0a8409751..a07297be28 100644 --- a/services/frontend/src/components/StudentNameVisibilityToggle/index.jsx +++ b/services/frontend/src/components/StudentNameVisibilityToggle/index.tsx @@ -1,11 +1,12 @@ +import { FormControlLabel, Switch } from '@mui/material' import { useCallback } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { Radio } from 'semantic-ui-react' +import { RootState } from '@/redux' import { toggleStudentNameVisibility } from '@/redux/settings' export const useStudentNameVisibility = () => { - const visible = useSelector(state => state.settings.namesVisible) + const visible = useSelector((state: RootState) => state.settings.namesVisible) const dispatch = useDispatch() const toggle = useCallback(() => { @@ -15,16 +16,18 @@ export const useStudentNameVisibility = () => { return { visible, toggle } } -export const StudentNameVisibilityToggle = ({ style = {} }) => { +export const StudentNameVisibilityToggle = () => { const { visible, toggle } = useStudentNameVisibility() const handleChange = useCallback(() => { toggle() - }, [visible, toggle]) + }, [toggle]) return ( -
- -
+ } + label="Show student names" + sx={{ margin: 0 }} + /> ) } diff --git a/services/frontend/src/components/StudentStatistics/StudentCourseTable/index.jsx b/services/frontend/src/components/StudentStatistics/StudentCourseTable/index.jsx deleted file mode 100644 index dc3ef5671d..0000000000 --- a/services/frontend/src/components/StudentStatistics/StudentCourseTable/index.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import { array, arrayOf, string } from 'prop-types' -import { Table } from 'semantic-ui-react' - -const courseNameColumnIndex = 1 -const creditsColumnIndex = 3 - -const getHeaderRow = headers => ( - - - {headers.map((header, index) => ( - - {header} - - ))} - - -) - -const getTableBody = rows => ( - - {rows.map((row, index) => { - const [highlight, ...rest] = Object.values(row) - const style = highlight ? { backgroundColor: '#e8f4ff' } : null - return ( - - {rest.map((value, index) => ( - - {value} - - ))} - - ) - })} - -) - -export const StudentCourseTable = ({ headers, rows }) => { - if (rows.length > 0) { - return ( - - {getHeaderRow(headers)} - {getTableBody(rows)} -
- ) - } - return
Student has courses marked
-} - -StudentCourseTable.propTypes = { - headers: arrayOf(string).isRequired, - rows: arrayOf(array).isRequired, -} diff --git a/services/frontend/src/components/StudentStatistics/StudentDetails/CourseParticipationTable.jsx b/services/frontend/src/components/StudentStatistics/StudentDetails/CourseParticipationTable.jsx deleted file mode 100644 index a25384dbd6..0000000000 --- a/services/frontend/src/components/StudentStatistics/StudentDetails/CourseParticipationTable.jsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Fragment } from 'react' -import { Link } from 'react-router' -import { Divider, Header, Icon, Item, Label } from 'semantic-ui-react' - -import { getTextInWithOpen } from '@/common' -import { useLanguage } from '@/components/LanguagePicker/useLanguage' -import { StudentCourseTable } from '@/components/StudentStatistics/StudentCourseTable' -import { DISPLAY_DATE_FORMAT } from '@/constants/date' -import { reformatDate } from '@/util/timeAndDate' - -// Some courses are without AY in the beginning in the studyplan even though the credits are registered with AY. -const isInStudyPlan = (plan, code) => - plan && (plan.included_courses.includes(code) || plan.included_courses.includes(code.replace('AY', ''))) - -const getAcademicYear = date => { - const year = new Date(date).getFullYear() - const month = new Date(date).getMonth() - // Months are 0-indexed so 7 means August... - return month < 7 ? `${year - 1}-${year}` : `${year}-${year + 1}` -} - -const getIcon = (credittypecode, isStudyModuleCredit, passed) => { - const style = { overflow: 'visible' } - if (isStudyModuleCredit) return - if (credittypecode === 9) return - return passed ? : -} - -export const CourseParticipationTable = ({ student, selectedStudyPlanId }) => { - const { getTextIn } = useLanguage() - if (!student) return null - - const studyPlan = student?.studyplans.find(plan => plan.id === selectedStudyPlanId) - - const courseRowsByAcademicYear = student.courses.reduceRight((acc, attainment) => { - const { course, credits, credittypecode, date, grade, isOpenCourse, isStudyModuleCredit, passed } = attainment - const isIncluded = isInStudyPlan(studyPlan, course.code) - const academicYear = getAcademicYear(date) - - if (!acc[academicYear]) acc[academicYear] = [] - - acc[academicYear].push([ - isIncluded, - reformatDate(date, DISPLAY_DATE_FORMAT), -
- {getTextInWithOpen(course, getTextIn, isOpenCourse, isStudyModuleCredit)} - {credittypecode === 7 && ( -
, -
- {getIcon(credittypecode, isStudyModuleCredit, passed)} - {grade} -
, - credits, - - - , - ]) - return acc - }, {}) - - return ( - <> - -
Courses
-
- {Object.entries(courseRowsByAcademicYear).map(([academicYear, courses]) => ( - -
- - - ))} - - ) -} diff --git a/services/frontend/src/components/StudentStatistics/StudentDetails/TagsTable.jsx b/services/frontend/src/components/StudentStatistics/StudentDetails/TagsTable.jsx deleted file mode 100644 index a6ec225797..0000000000 --- a/services/frontend/src/components/StudentStatistics/StudentDetails/TagsTable.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import { sortBy } from 'lodash' -import { shape } from 'prop-types' -import { Header, Label } from 'semantic-ui-react' - -import { useLanguage } from '@/components/LanguagePicker/useLanguage' -import { SortableTable } from '@/components/SortableTable' - -export const TagsTable = ({ student }) => { - const { getTextIn } = useLanguage() - if (!student) return null - - const data = Object.values( - student.tags.reduce((acc, tag) => { - if (!acc[tag.programme.code]) acc[tag.programme.code] = { programme: tag.programme, tags: [] } - acc[tag.tag.studytrack].tags.push(tag) - return acc - }, {}) - ) - if (data.length === 0) return null - return ( - <> -
- getTextIn(tag.programme.name), - }, - { - key: 'CODE', - title: 'Code', - getRowVal: tag => tag.programme.code, - }, - { - key: 'TAGS', - title: 'Tags', - getRowVal: tag => sortBy(tag.tags.map(tt => tt.tag.tagname)).join(':'), - getRowContent: tag => - sortBy(tag.tags, tag => tag.tag.tagname).map(tag => ( -