From 200886e57f3225fd4f73c476acb8e4142781f0b3 Mon Sep 17 00:00:00 2001 From: AmirHossein Haerian <44199492+amirhossein-haerian@users.noreply.github.com> Date: Thu, 17 Oct 2024 09:31:06 +0200 Subject: [PATCH 1/5] Issues/kp 367 using ladok for search courses (#297) * feat(KP-367): the search is now updated with the new data comming from ladok * feat(KP-367): search filters are now working as expected in phase 1 * feat(KP-367): formatted course credits has been used * fix(KP-367): one comment is added * fix(KP-367): change condition for triggering noQueryProvidedDispatch * fix(KP-367): add errorUnKnown as default error message for alerts * the front-end is updated after the upgrade in grouping results from ladok * feat(KP-367): search parameters chnged to only semesters * feat(KP-367): search parameters are changed and the task is ready * change the ladok API usage based on changes in ladok mellan lager --- .env.in | 6 +- config/commonSettings.js | 1 + config/serverSettings.js | 13 + domain/searchParams.js | 34 +- domain/term.js | 7 + i18n/messages.en.js | 5 + i18n/messages.se.js | 5 + package-lock.json | 20 +- package.json | 3 +- .../app/components/NewSearchOptions/index.tsx | 2 +- .../app/components/NewSearchOptions/types.ts | 10 +- .../NewSearchResultDisplay/ListView.test.tsx | 31 +- .../NewSearchResultDisplay/ListView.tsx | 140 +- .../NewSearchResultDisplay/TableView.test.tsx | 99 +- .../NewSearchResultDisplay/TableView.tsx | 12 +- .../NewSearchResultDisplay/index.tsx | 8 +- .../NewSearchResultDisplay/style.scss | 95 +- .../NewSearchResultDisplay/types.ts | 8 +- .../js/app/components/SearchAlert/index.tsx | 2 +- .../SearchFilters/SearchFilters.test.tsx | 4 +- .../js/app/components/SearchFilters/index.tsx | 28 +- .../js/app/components/SearchFilters/types.ts | 7 +- .../app/components/mocks/mockCourseSeasrch.ts | 300 +++ .../js/app/components/mocks/mockSearchHits.ts | 1898 ++++++++++++++++- .../hooks/tests/useLangHrefUpdates.test.tsx | 6 +- public/js/app/hooks/useCourseSearch.ts | 2 +- public/js/app/hooks/useCourseSearchParams.ts | 4 +- public/js/app/pages/NewSearchLandingPage.tsx | 2 +- public/js/app/pages/NewSearchPage.tsx | 8 +- .../js/app/pages/SearchLandingPage.test.tsx | 16 +- public/js/app/pages/SearchPage.test.tsx | 145 +- .../pages/mocks/Appendix1ApplicationStore.js | 6 + public/js/app/pages/types/searchPageTypes.ts | 11 +- .../app/stores/types/searchPageStoreTypes.ts | 2 + public/js/app/util/internApi.js | 4 +- public/js/app/util/newSearchHelper.ts | 175 +- public/js/app/util/searchApi.ts | 16 +- public/js/app/util/types/SearchApiTypes.ts | 11 +- public/js/app/util/types/SearchHelperTypes.ts | 13 +- server/controllers/newSearchPageCtrl.js | 55 +- server/ladok/ladokApi.js | 14 + server/server.js | 7 +- 42 files changed, 2871 insertions(+), 364 deletions(-) create mode 100644 public/js/app/components/mocks/mockCourseSeasrch.ts create mode 100644 server/ladok/ladokApi.js diff --git a/.env.in b/.env.in index a491e163..b6447b40 100644 --- a/.env.in +++ b/.env.in @@ -1,3 +1,7 @@ PDF_PROGRAM_SYLLABUS_URL= [Available in Azure KeyVault] PDF_RENDER_FUNCTION_SUBSCRIPTION_KEY= [Available in Azure KeyVault] -REDIS_URI= [Available in Azure KeyVault] \ No newline at end of file +REDIS_URI= [Available in Azure KeyVault] + +# Ladok Mellanlager connection +LADOK_AUTH_CLIENT_SECRET=[Available in Azure KeyVault] +LADOK_OCP_APIM_SUBSCRIPTION_KEY=[Available in Azure KeyVault] \ No newline at end of file diff --git a/config/commonSettings.js b/config/commonSettings.js index 484e7aa9..839cf237 100644 --- a/config/commonSettings.js +++ b/config/commonSettings.js @@ -19,6 +19,7 @@ module.exports = { newSearchPage: `${studentRoot}/sokkurs-beta`, searchResult: `${studentRoot}/sokkurs-beta/resultat`, courseSearchInternApi: `${studentRoot}/intern-api/sok`, + courseSearchInternApiBeta: `${studentRoot}/intern-api/sokBeta`, department: `${studentRoot}/org`, programme: `${studentRoot}/program`, programmesList: `${studentRoot}/kurser-inom-program`, diff --git a/config/serverSettings.js b/config/serverSettings.js index 12cda807..bb6111a0 100644 --- a/config/serverSettings.js +++ b/config/serverSettings.js @@ -46,6 +46,19 @@ module.exports = { // Kopps koppsApi: unpackKOPPSConfig('KOPPS_API_BASEURL', devKoppsApi), + // TODO(Ladok-POC): Replace devDefaults and add values to ref/prod.parameters.json when final mellanlager is deployed + ladokMellanlagerApi: { + clientId: getEnv('LADOK_AUTH_CLIENT_ID', devDefaults('c978bff4-80c6-42d2-8d64-a6d90227013b')), + clientSecret: getEnv('LADOK_AUTH_CLIENT_SECRET', null), + tokenUrl: getEnv( + 'LADOK_AUTH_TOKEN_URL', + devDefaults('https://login.microsoftonline.com/acd7f330-d613-48d9-85f2-258b1ac4a015/oauth2/v2.0/token') + ), + scope: getEnv('LADOK_AUTH_SCOPE', devDefaults('api://4afd7e46-019e-44e1-9630-12fdf9d31d02/.default')), + baseUrl: getEnv('LADOK_BASE_URL', devDefaults('https://ladok-mellanlagring-lab.azure-api.net')), + ocpApimSubscriptionKey: getEnv('LADOK_OCP_APIM_SUBSCRIPTION_KEY', null), + }, + // Service API's nodeApi: { // nodeApi: unpackNodeApiConfig('NODE_API_URI', devNodeApi), diff --git a/domain/searchParams.js b/domain/searchParams.js index 8c724d46..39b1a824 100644 --- a/domain/searchParams.js +++ b/domain/searchParams.js @@ -102,7 +102,7 @@ function _combineTermsByYear(arrWithYearsAndPeriod) { return groupedTerms } -function _periodConfigForOneYear({ year, terms }, langIndex) { +function _periodConfigForOneYear({ year, terms }, langIndex, isBetaSearch) { const hasOnlyOneTerm = !!terms.length === 1 const { summer: summerLabel } = i18n.messages[langIndex].bigSearch @@ -130,7 +130,7 @@ function _periodConfigForOneYear({ year, terms }, langIndex) { value, }) } - const value = `${year}${term}:${periodNum}` + const value = isBetaSearch ? `${term == 2 ? 'HT' : 'VT'}${year}:${periodNum}` : `${year}${term}:${periodNum}` const label = `${formatLongTerm(`${year}${term}`, language)} period ${periodNum}` return resultPeriodsConfig.push({ label, id: value, value }) }) @@ -139,20 +139,38 @@ function _periodConfigForOneYear({ year, terms }, langIndex) { return resultPeriodsConfig } -function _periodConfigByYearType(yearType, langIndex) { +function _periodConfigByYearType(yearType, langIndex, isBetaSearch = false) { const relevantTerms = getRelevantTerms(2) const yearsAndPeriod = _separateYearAndPeriod(relevantTerms) const { current, next } = _combineTermsByYear(yearsAndPeriod) switch (yearType) { case 'currentYear': - return _periodConfigForOneYear(current, langIndex) + return _periodConfigForOneYear(current, langIndex, isBetaSearch) case 'nextYear': - return _periodConfigForOneYear(next, langIndex) + return _periodConfigForOneYear(next, langIndex, isBetaSearch) default: throw new Error(`Unknown yearType: ${yearType}. Allowed values: currentYear and nextYear`) } } +function _semestersConfig(langIndex) { + const { messages } = i18n.messages[langIndex] + const { semester } = messages + const relevantTerms = getRelevantTerms(2) + const yearsAndPeriod = _separateYearAndPeriod(relevantTerms) + const resultSemestersConfig = [] + yearsAndPeriod.forEach(({ year, termNumber }) => { + const label = `${semester[termNumber]} ${year}` + const value = `${termNumber === '1' ? 'VT' : 'HT'}${year}` + resultSemestersConfig.push({ + label, + id: value, + value, + }) + }) + return resultSemestersConfig +} + function getParamConfig(paramName, langIndex) { switch (paramName) { case 'eduLevel': @@ -161,6 +179,12 @@ function getParamConfig(paramName, langIndex) { return _periodConfigByYearType('currentYear', langIndex) case 'nextYear': return _periodConfigByYearType('nextYear', langIndex) + case 'currentYearBeta': + return _periodConfigByYearType('currentYear', langIndex, true) + case 'nextYearBeta': + return _periodConfigByYearType('nextYear', langIndex, true) + case 'semesters': + return _semestersConfig(langIndex) case 'showOptions': return showOptionsConfig(langIndex) case 'onlyMHU': diff --git a/domain/term.js b/domain/term.js index bcfca265..1d43357a 100644 --- a/domain/term.js +++ b/domain/term.js @@ -91,6 +91,12 @@ function formatShortTerm(term, language) { return `${t('semester')[semester]}${language === 'en' ? ' ' : ''}${shortYear}` } +function formatTermByYearAndPeriod(period, year, language) { + const t = translate(language) + const shortYear = year.toString().slice(-2) + return `${t('semester')[period == 0 || period == 1 || period == 2 ? 2 : 1]}${language === 'en' ? ' ' : ''}${shortYear}` +} + function formatLongTerm(term, language) { const t = translate(language) const [year, semester] = splitTerm(term) @@ -153,6 +159,7 @@ module.exports = { _nTermsAgo, studyYear: _studyYear, formatShortTerm, + formatTermByYearAndPeriod, formatLongTerm, splitTerm, add, diff --git a/i18n/messages.en.js b/i18n/messages.en.js index 7a71f0e5..de60526b 100644 --- a/i18n/messages.en.js +++ b/i18n/messages.en.js @@ -60,6 +60,9 @@ const messages = { course_name: 'Course name', course_educational_level: 'Educational level', course_educational_level_abbr: 'Edu. level', + course_language: 'Language', + course_campus: 'Campus', + course_pace: 'Pace', study_year: 'Study year', @@ -244,6 +247,7 @@ const messages = { leadIntro: 'Find info on KTH courses: course syllabus, course memo, and course analyses. Search by course name or course code, you can also filter by semester and period. Which courses are included in a program can be found under Programme syllabuses.', eduLevel: 'Educational level:', + semesters: 'Course Start:', PREPARATORY: 'Pre-university level', BASIC: 'First cycle', ADVANCED: 'Second cycle', @@ -337,6 +341,7 @@ const messages = { }, searchAlarms: { errorUnknown: { text: 'An unknown error occurred - failed to retrieve course data' }, + errorKodEllerBenamning: { text: 'Search input must be equal or more than 3 characters.' }, errorEmpty: { header: 'Your search returned no results', help: 'For help, see the link below: Instructions for searching', diff --git a/i18n/messages.se.js b/i18n/messages.se.js index 33fa0ba4..f2e383ff 100644 --- a/i18n/messages.se.js +++ b/i18n/messages.se.js @@ -59,6 +59,9 @@ const messages = { course_name: 'Kursnamn', course_educational_level: 'Utbildningsnivå', course_educational_level_abbr: 'Utbildningsnivå', + course_language: 'Språk', + course_campus: 'Campus', + course_pace: 'Omfattning', study_year: 'Årskurs', @@ -238,6 +241,7 @@ const messages = { leadIntro: 'Här finns info om kurser på KTH: kursplan, kurs-PM och kursanalyser. Sök på kursnamn eller kurskod, du kan även filtrera på termin och läsperiod. Vilka kurser som ingår i ett program finns under Utbildningsplaner.', eduLevel: 'Utbildningsnivå:', + semesters: 'Kursstart:', PREPARATORY: 'Förberedande nivå', BASIC: 'Grundnivå', ADVANCED: 'Avancerad nivå', @@ -328,6 +332,7 @@ const messages = { }, searchAlarms: { errorUnknown: { text: 'Ett okänt fel inträffade - misslyckad hämtning av kursdata' }, + errorKodEllerBenamning: { text: 'Sökinmatningen måste vara lika med eller större än 3 tecken.' }, errorEmpty: { header: 'Din sökning gav inga träffar.', help: 'Mer hjälp hittar du i länken nedan: Få hjälp med sökningen', diff --git a/package-lock.json b/package-lock.json index 99bd00b6..cec17827 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@kth/monitor": "^4.2.1", "@kth/server": "^4.1.0", "@kth/session": "^3.0.9", - "@kth/style": "^1.4.2", + "@kth/style": "^1.6.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "axios": "^1.6.7", @@ -36,6 +36,7 @@ "kth-style": "^10.3.0", "mobx": "^6.12.0", "mobx-react": "^9.1.0", + "om-kursen-ladok-client": "file:../studadm-om-kursen-packages/packages/om-kursen-ladok-client", "prop-types": "^15.8.1", "querystring": "^0.2.1", "react": "^18.2.0", @@ -92,6 +93,13 @@ "node": "18" } }, + "../studadm-om-kursen-packages/packages/om-kursen-ladok-client": { + "version": "0.0.1", + "dependencies": { + "ladok-attributvarde-utils": "file:../ladok-attributvarde-utils", + "ladok-mellanlager-client": "file:../ladok-mellanlager-client" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -3392,9 +3400,9 @@ } }, "node_modules/@kth/style": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@kth/style/-/style-1.4.2.tgz", - "integrity": "sha512-Z6EhjUVzhw+QITpnFsdLu51xogsgEXQiB1E+hEI2HsaFxTOrxkq6qbnN/uK1WWeTuKfi+A6szaVubPXswWRtyw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@kth/style/-/style-1.6.0.tgz", + "integrity": "sha512-KSngUH4TGB4ztRcaTcoI0eoi0yKN3my1lzpJ9iwxldzwvO552W4R6YCi2GvdjW1JAmadI3C/aRAOTWFNOSE1Ug==", "peerDependencies": { "react": "*" } @@ -14019,6 +14027,10 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/om-kursen-ladok-client": { + "resolved": "../studadm-om-kursen-packages/packages/om-kursen-ladok-client", + "link": true + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", diff --git a/package.json b/package.json index 26507dba..04ca4e98 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@kth/monitor": "^4.2.1", "@kth/server": "^4.1.0", "@kth/session": "^3.0.9", - "@kth/style": "^1.4.2", + "@kth/style": "^1.6.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "axios": "^1.6.7", @@ -66,6 +66,7 @@ "kth-style": "^10.3.0", "mobx": "^6.12.0", "mobx-react": "^9.1.0", + "om-kursen-ladok-client": "file:../studadm-om-kursen-packages/packages/om-kursen-ladok-client", "prop-types": "^15.8.1", "querystring": "^0.2.1", "react": "^18.2.0", diff --git a/public/js/app/components/NewSearchOptions/index.tsx b/public/js/app/components/NewSearchOptions/index.tsx index 6aedabf8..6d1c034b 100644 --- a/public/js/app/components/NewSearchOptions/index.tsx +++ b/public/js/app/components/NewSearchOptions/index.tsx @@ -20,7 +20,7 @@ const SearchOptions: React.FC = ({ const searchHeadLevel = overrideSearchHead || bigSearch[paramName] const values: SearchOptionConfig[] = useMemo( - () => getParamConfig(paramAliasName || paramName, languageIndex), + () => getParamConfig(paramAliasName || paramName, languageIndex) || [], [paramAliasName, paramName, languageIndex] ) diff --git a/public/js/app/components/NewSearchOptions/types.ts b/public/js/app/components/NewSearchOptions/types.ts index 646fac49..bb15437c 100644 --- a/public/js/app/components/NewSearchOptions/types.ts +++ b/public/js/app/components/NewSearchOptions/types.ts @@ -1,11 +1,11 @@ -import { EduLevel, Period, ShowOptions } from '../../stores/types/searchPageStoreTypes' +import { EduLevel, Period, Semester, ShowOptions } from '../../stores/types/searchPageStoreTypes' -export type SearchOptionValues = (EduLevel | Period | ShowOptions)[] +export type SearchOptionValues = (EduLevel | Semester | Period | ShowOptions)[] export interface SearchOptionConfig { label: string id: string - value: EduLevel | Period | ShowOptions + value: EduLevel | Semester | Period | ShowOptions } export interface SearchOptionReturnValues { @@ -14,8 +14,8 @@ export interface SearchOptionReturnValues { export interface SearchOptionsProps { overrideSearchHead?: string - paramAliasName?: 'currentYear' | 'nextYear' | 'onlyMHU' | '' - paramName: 'eduLevel' | 'period' | 'showOptions' + paramAliasName?: 'currentYear' | 'nextYear' | 'currentYearBeta' | 'nextYearBeta' | 'onlyMHU' | '' + paramName: 'eduLevel' | 'semesters' | 'period' | 'showOptions' selectedValues: SearchOptionValues onChange: (params: SearchOptionReturnValues) => void disabled?: boolean diff --git a/public/js/app/components/NewSearchResultDisplay/ListView.test.tsx b/public/js/app/components/NewSearchResultDisplay/ListView.test.tsx index 319d1866..2b10cf95 100644 --- a/public/js/app/components/NewSearchResultDisplay/ListView.test.tsx +++ b/public/js/app/components/NewSearchResultDisplay/ListView.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react' import '@testing-library/jest-dom' import ListView from './ListView' import { useStore } from '../../mobx' -import { TEST_API_ANSWER_RESOLVED } from '../mocks/mockKoppsCourseSearch' +import { TEST_API_ANSWER_RESOLVED } from '../mocks/mockCourseSeasrch' jest.mock('../../mobx') @@ -28,10 +28,13 @@ describe('ListView component', () => { searchHits: [ { ...TEST_API_ANSWER_RESOLVED.searchHits[0], - searchHitInterval: { - startPeriod: null as any, - endPeriod: null as any, - }, + period: [ + { + forstaUndervisningsdatum: { + period: null as any, + }, + }, + ], }, ], } @@ -61,22 +64,4 @@ describe('ListView component', () => { expect(screen.getByText(/P1 Autumn 21 - P3 Spring 22/i)).toBeInTheDocument() }) - - test('renders course with a specific credit unit abbreviation', async () => { - const modifiedResultss = { - searchHits: [ - { - course: { - ...TEST_API_ANSWER_RESOLVED.searchHits[0].course, - credits: '7.5', - creditUnitAbbr: 'ECTS', - }, - }, - ], - } - - render() - - expect(screen.getByText(/ECTS/i)).toBeInTheDocument() - }) }) diff --git a/public/js/app/components/NewSearchResultDisplay/ListView.tsx b/public/js/app/components/NewSearchResultDisplay/ListView.tsx index 191cdc55..29a00000 100644 --- a/public/js/app/components/NewSearchResultDisplay/ListView.tsx +++ b/public/js/app/components/NewSearchResultDisplay/ListView.tsx @@ -6,57 +6,133 @@ import { useStore } from '../../mobx' import { ListViewParams } from './types' import i18n from '../../../../../i18n' -import { compareCoursesBy, flatCoursesArr, inforKursvalLink, periodsStr } from '../../util/newSearchHelper' +import { compareCoursesBy, inforKursvalLink, periodsStr } from '../../util/newSearchHelper' const ListView: React.FC = ({ results }) => { const { language, languageIndex } = useStore() - const { generalSearch } = i18n.messages[languageIndex] + const { generalSearch, messages } = i18n.messages[languageIndex] + + const { course_pace, course_campus, course_language } = messages const { courseHasNoRounds, linkToInforKursval } = generalSearch - const { courses, hasSearchHitInterval } = flatCoursesArr(results) return ( <> - {courses.sort(compareCoursesBy('courseCode')).map((course, index) => { - const { courseCode, title, credits, creditUnitAbbr, startTerm, endTerm, endPeriod, startPeriod } = course + {results.sort(compareCoursesBy('kod')).map((course, index) => { + const { + kod: courseCode, + benamning: title, + omfattning: { formattedWithUnit: credits = '' } = {}, + period: periods = [], + startperiod: startPeriods = [], + studietakt: studyPaces = [], + undervisningssprak: languages = [], + studieort: campuses = [], + } = course || {} + + const allPeriods = periods.map( + ({ + startperiod: { inDigits: startTerm = '' } = {}, + forstaUndervisningsdatum: { + date: startDate = '', + year: startPeriodYear = '', + week: startWeek = '', + period: startPeriod = '', + } = {}, + sistaUndervisningsdatum: { + date: endDate = '', + year: endPeriodYear = '', + week: endWeek = '', + period: endPeriod = '', + } = {}, + tillfallesperioderNummer = undefined, + }) => ({ + startTerm, + startDate, + startPeriodYear, + startWeek, + startPeriod, + endDate, + endPeriodYear, + endWeek, + endPeriod, + tillfallesperioderNummer, + }) + ) + + const allStudyPaces = studyPaces.map(({ takt: coursePace = '' }) => ({ + coursePace, + })) + + const allLanguages = languages.map(({ name: courseLanguage = '' }) => ({ + courseLanguage, + })) + + const allCampuses = campuses.map(({ name: courseCampus = '' }) => ({ + courseCampus, + })) + + const allStartPeriods = startPeriods.map(({ code: startTerm = '', inDigits = '' }) => ({ + startTerm, + inDigits, + })) + + const startTerm = allStartPeriods.length === 1 ? allStartPeriods[0].startTerm : undefined + + let periodTexts = [] + periodTexts = allPeriods.map( + ({ + startPeriod, + startPeriodYear, + endPeriod, + endPeriodYear, + tillfallesperioderNummer, + }: { + startPeriod: string + startPeriodYear: number + endPeriod: string + endPeriodYear: number + tillfallesperioderNummer: number + }) => periodsStr(startPeriod, startPeriodYear, endPeriod, endPeriodYear, tillfallesperioderNummer, language) + ) + + const areAllPeriodTextsEmpty = periodTexts.every((value: string) => !value.trim()) - let periodText = undefined - if (hasSearchHitInterval) { - periodText = periodsStr(startPeriod, startTerm, endPeriod, endTerm, language) - } const InforKursvalLink = inforKursvalLink(linkToInforKursval, courseCode, startTerm, language) return (

- {title}, {credits} {creditUnitAbbr} + {title}, {credits}

{courseCode} - {periodText && {periodText}} - {periodText === '' && {courseHasNoRounds}} + + {periodTexts.map((periodText: string, index: number) => `${index != 0 ? ',' : ''} ${periodText}`)} + + {(areAllPeriodTextsEmpty || periodTexts.length === 0) && {courseHasNoRounds}}
- {/* -

- we dont have it now -

- */} -
- {/* -
- Location Icon - -
-
- Language Icon - +
+
+
+ {course_campus} + {allCampuses.map(({ courseCampus }: { courseCampus: string }, index: number) => ( + {courseCampus} + ))} +
+
+ {course_language} + {allLanguages.map(({ courseLanguage }: { courseLanguage: string }, index: number) => ( + {courseLanguage} + ))} +
+
+ {course_pace} + {allStudyPaces.map(({ coursePace }: { coursePace: number }, index: number) => ( + {coursePace}% + ))} +
-
- Pace Icon - -
- We don't have them now - */}
{InforKursvalLink}
diff --git a/public/js/app/components/NewSearchResultDisplay/TableView.test.tsx b/public/js/app/components/NewSearchResultDisplay/TableView.test.tsx index 67b1a826..34d1f560 100644 --- a/public/js/app/components/NewSearchResultDisplay/TableView.test.tsx +++ b/public/js/app/components/NewSearchResultDisplay/TableView.test.tsx @@ -4,28 +4,25 @@ import '@testing-library/jest-dom' import TableView from './TableView' import { useStore } from '../../mobx' import { - EXPECTED_TEST_SEARCH_HITS_MIXED_EN, - EXPECTED_TEST_SEARCH_HITS_MIXED_SV, - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN, - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV, - TEST_SEARCH_HITS_MIXED_EN, - TEST_SEARCH_HITS_MIXED_SV, - TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV, - TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN, - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN_new, - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV_new, + EXPECTED_TEST_SEARCH_HITS_MIXED_EN_BETA, + EXPECTED_TEST_SEARCH_HITS_MIXED_SV_BETA, + TEST_SEARCH_HITS_MIXED_EN_BETA, + TEST_SEARCH_HITS_MIXED_SV_BETA, + TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV_BETA, + TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN_BETA, + EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN_BETA, + EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV_BETA, } from '../mocks/mockSearchHits' jest.mock('../../mobx') -const reseacrhHitsColHeaders = { - en: ['Course code', 'Course name', 'Scope', 'Educational level'], - sv: ['Kurskod', 'Kursnamn', 'Omfattning', 'Utbildningsnivå'], -} -const mixedHitsColHeaders = { - en: [...reseacrhHitsColHeaders.en, 'Periods'], - sv: [...reseacrhHitsColHeaders.sv, 'Perioder'], +const headers = { + en: ['Course code', 'Course name', 'Scope', 'Educational level', 'Language', 'Pace', 'Campus', 'Periods'], + sv: ['Kurskod', 'Kursnamn', 'Omfattning', 'Utbildningsnivå', 'Språk', 'Omfattning', 'Campus', 'Perioder'], } +const reseacrhHitsColHeaders = headers +const mixedHitsColHeaders = headers + const eduLevelTranslations = { EN: { PREPARATORY: 'Pre-university level', BASIC: 'First cycle', ADVANCED: 'Second cycle', RESEARCH: 'Third cycle' }, SV: { PREPARATORY: 'Förberedande nivå', BASIC: 'Grundnivå', ADVANCED: 'Avancerad nivå', RESEARCH: 'Forskarnivå' }, @@ -35,12 +32,12 @@ describe('Component for RESEARCH courses', () => { test('creates a table with 4 columns for RESEARCH courses (without column for period intervals). English. 1A', () => { ;(useStore as jest.Mock).mockReturnValue({ language: 'en', languageIndex: 0 }) - render() + render() const rows = screen.queryAllByRole('row') const columnHeaders = screen.getAllByRole('columnheader') expect(rows).toHaveLength(4) // 3 courses + 1 header row - expect(columnHeaders).toHaveLength(4) + expect(columnHeaders).toHaveLength(8) columnHeaders.forEach((colHeader, index) => { expect(colHeader).toHaveTextContent(reseacrhHitsColHeaders.en[index]) @@ -48,10 +45,10 @@ describe('Component for RESEARCH courses', () => { rows.slice(1).forEach((row, index) => { const utils = within(row) - const { course } = TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN.searchHits[index] - expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.courseCode) - expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.title) - expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.credits} ${course.creditUnitAbbr}`) + const course = TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN_BETA.searchHits[index] + expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.kod) + expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.benamning) + expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.omfattning.formattedWithUnit}`) expect(utils.getAllByRole('cell')[3]).toHaveTextContent('Third cycle') }) }) @@ -59,12 +56,12 @@ describe('Component for RESEARCH courses', () => { test('creates a table with 4 columns for RESEARCH courses (without column for period intervals). Swedish. 2A', () => { ;(useStore as jest.Mock).mockReturnValue({ language: 'sv', languageIndex: 1 }) - render() + render() const rows = screen.queryAllByRole('row') const columnHeaders = screen.getAllByRole('columnheader') expect(rows).toHaveLength(4) // 3 courses + 1 header row - expect(columnHeaders).toHaveLength(4) + expect(columnHeaders).toHaveLength(8) columnHeaders.forEach((colHeader, index) => { expect(colHeader).toHaveTextContent(reseacrhHitsColHeaders.sv[index]) @@ -72,10 +69,10 @@ describe('Component for RESEARCH courses', () => { rows.slice(1).forEach((row, index) => { const utils = within(row) - const { course } = TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV.searchHits[index] - expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.courseCode) - expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.title) - expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.credits} ${course.creditUnitAbbr}`) + const course = TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV_BETA.searchHits[index] + expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.kod) + expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.benamning) + expect(utils.getAllByRole('cell')[2]).toHaveTextContent(course.omfattning.formattedWithUnit) expect(utils.getAllByRole('cell')[3]).toHaveTextContent('Forskarnivå') }) }) @@ -85,12 +82,12 @@ describe('Component for MIXED types of courses', () => { test('creates a table with 5 columns for mixed types of courses (including column for period intervals). English. 1B', () => { ;(useStore as jest.Mock).mockReturnValue({ language: 'en', languageIndex: 0 }) - render() + render() const rows = screen.queryAllByRole('row') const columnHeaders = screen.getAllByRole('columnheader') expect(rows).toHaveLength(6) // 5 courses + 1 header row - expect(columnHeaders).toHaveLength(5) + expect(columnHeaders).toHaveLength(8) columnHeaders.forEach((colHeader, index) => { expect(colHeader).toHaveTextContent(mixedHitsColHeaders.en[index]) @@ -98,26 +95,29 @@ describe('Component for MIXED types of courses', () => { rows.slice(1).forEach((row, index) => { const utils = within(row) - const { course } = EXPECTED_TEST_SEARCH_HITS_MIXED_EN.searchHits[index] - expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.courseCode) - expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.title) - expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.credits} ${course.creditUnitAbbr}`) - expect(utils.getAllByRole('cell')[3]).toHaveTextContent( - course.educationalLevel ? eduLevelTranslations.EN[course.educationalLevel] : '' + const course = EXPECTED_TEST_SEARCH_HITS_MIXED_EN_BETA.searchHits[index] + expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.kod) + expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.benamning) + expect(utils.getAllByRole('cell')[2]).toHaveTextContent(course.omfattning.formattedWithUnit) + expect(utils.getAllByRole('cell')[3]).toHaveTextContent(course.utbildningstyp[0].level.name) + expect(utils.getAllByRole('cell')[4]).toHaveTextContent(course.undervisningssprak[0].name) + expect(utils.getAllByRole('cell')[5]).toHaveTextContent(`${course.studietakt[0].code}%`) + expect(utils.getAllByRole('cell')[6]).toHaveTextContent(course.studieort[0].name) + expect(utils.getAllByRole('cell')[7]).toHaveTextContent( + EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN_BETA[index] ) - expect(utils.getAllByRole('cell')[4]).toHaveTextContent(EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN_new[index]) }) }) - test('creates a table with 5 columns for MIXED types of courses (including column for period intervals). Swedish. 2B', () => { + test('creates a table with 8 columns for MIXED types of courses (including column for period intervals). Swedish. 2B', () => { ;(useStore as jest.Mock).mockReturnValue({ language: 'sv', languageIndex: 1 }) - render() + render() const rows = screen.queryAllByRole('row') const columnHeaders = screen.getAllByRole('columnheader') expect(rows).toHaveLength(6) // 5 courses + 1 header row - expect(columnHeaders).toHaveLength(5) + expect(columnHeaders).toHaveLength(8) columnHeaders.forEach((colHeader, index) => { expect(colHeader).toHaveTextContent(mixedHitsColHeaders.sv[index]) @@ -125,14 +125,17 @@ describe('Component for MIXED types of courses', () => { rows.slice(1).forEach((row, index) => { const utils = within(row) - const { course } = EXPECTED_TEST_SEARCH_HITS_MIXED_SV.searchHits[index] - expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.courseCode) - expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.title) - expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.credits} ${course.creditUnitAbbr}`) - expect(utils.getAllByRole('cell')[3]).toHaveTextContent( - course.educationalLevel ? eduLevelTranslations.SV[course.educationalLevel] : '' + const course = EXPECTED_TEST_SEARCH_HITS_MIXED_SV_BETA.searchHits[index] + expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.kod) + expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.benamning) + expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.omfattning.formattedWithUnit}`) + expect(utils.getAllByRole('cell')[3]).toHaveTextContent(course.utbildningstyp[0].level.name) + expect(utils.getAllByRole('cell')[4]).toHaveTextContent(course.undervisningssprak[0].name) + expect(utils.getAllByRole('cell')[5]).toHaveTextContent(`${course.studietakt[0].code}%`) + expect(utils.getAllByRole('cell')[6]).toHaveTextContent(course.studieort[0].name) + expect(utils.getAllByRole('cell')[7]).toHaveTextContent( + EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV_BETA[index] ) - expect(utils.getAllByRole('cell')[4]).toHaveTextContent(EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV_new[index]) }) }) }) diff --git a/public/js/app/components/NewSearchResultDisplay/TableView.tsx b/public/js/app/components/NewSearchResultDisplay/TableView.tsx index 51e5643c..b1ca63a5 100644 --- a/public/js/app/components/NewSearchResultDisplay/TableView.tsx +++ b/public/js/app/components/NewSearchResultDisplay/TableView.tsx @@ -5,7 +5,7 @@ import './style.scss' import { useStore } from '../../mobx' import translate from '../../../../../domain/translate' -import { flatCoursesArr, sortAndParseByCourseCodeForTableView } from '../../util/newSearchHelper' +import { sortAndParseByCourseCodeForTableView } from '../../util/newSearchHelper' import { TableViewParams } from './types' import { SortableTable } from '@kth/kth-reactstrap/dist/components/studinfo' @@ -14,18 +14,18 @@ const TableView: React.FC = ({ results }) => { const { language, languageIndex } = useStore() const t = translate(language) - const { courses, hasSearchHitInterval } = flatCoursesArr(results) - - const sliceUntilNum = hasSearchHitInterval ? 5 : 4 const headers = [ t('course_code'), t('course_name'), t('course_scope'), t('course_educational_level'), + t('course_language'), + t('course_pace'), + t('course_campus'), t('department_period_abbr'), - ].slice(0, sliceUntilNum) + ] - const coursesForTableView = sortAndParseByCourseCodeForTableView(courses, sliceUntilNum, language) + const coursesForTableView = sortAndParseByCourseCodeForTableView(results, language) return (
diff --git a/public/js/app/components/NewSearchResultDisplay/index.tsx b/public/js/app/components/NewSearchResultDisplay/index.tsx index be855f79..df4d9f14 100644 --- a/public/js/app/components/NewSearchResultDisplay/index.tsx +++ b/public/js/app/components/NewSearchResultDisplay/index.tsx @@ -4,7 +4,7 @@ import './style.scss' import { useStore } from '../../mobx' -import { SearchResultDisplayParams, KoppsCourseSearchResult, VIEW, View } from './types' +import { SearchResultDisplayParams, CourseSearchResult, VIEW, View } from './types' import { STATUS } from '../../hooks/types/UseCourseSearchTypes' import Article from '../Article' import SearchAlert from '../SearchAlert' @@ -12,8 +12,8 @@ import SearchResultHeader from './SearchResultHeader' import SearchResultComponent from './SearchResultComponent' import { AlertType } from '../SearchAlert/types' -const isKoppsCourseSearchResult = (data: string | KoppsCourseSearchResult): data is KoppsCourseSearchResult => { - return (data as KoppsCourseSearchResult).searchHits !== undefined +const isCourseSearchResult = (data: string | CourseSearchResult): data is CourseSearchResult => { + return (data as CourseSearchResult).searchHits !== undefined } const NewSearchResultDisplay: React.FC = ({ resultsState }) => { const { languageIndex } = useStore() @@ -38,7 +38,7 @@ const NewSearchResultDisplay: React.FC = ({ resultsSt setView={setView} /> {searchStatus === STATUS.resolved && - isKoppsCourseSearchResult(searchResults) && + isCourseSearchResult(searchResults) && searchResults.searchHits.length > 0 && } ) diff --git a/public/js/app/components/NewSearchResultDisplay/style.scss b/public/js/app/components/NewSearchResultDisplay/style.scss index e6b64528..4a26f4ec 100644 --- a/public/js/app/components/NewSearchResultDisplay/style.scss +++ b/public/js/app/components/NewSearchResultDisplay/style.scss @@ -104,15 +104,103 @@ } } - .course-details { + .course-footer { background-color: var(--color-background-alt); display: flex; justify-content: space-between; align-items: center; - padding: space.$space-16; + padding: space.$space-8 space.$space-16; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; box-shadow: 2px 2px 5px 0px colors.$color-gray-light; + @media (max-width: 700px) { + flex-wrap: wrap; + row-gap: space.$space-16; + } + .course-details { + display: flex; + align-items: start; + justify-content: space-between; + font-weight: typography.$font-bold; + gap: space.$space-8; + @media (min-width: 1200px) { + width: 45%; + } + @media (min-width: 992px) and (max-width: 1200px) { + width: 50%; + } + @media (min-width: 700px) and (max-width: 992px) { + width: 50%; + } + @media (max-width: 700px) { + flex: 1 1 100%; + } + .course-location { + display: flex; + flex-direction: column; + justify-content: center; + .icon { + color: var(--color-tertiary); + display: flex; + justify-content: center; + align-items: center; + gap: space.$space-2; + font-weight: bold; + padding-bottom: space.$space-8; + &:before { + @include icons.icon-home; + width: 32px; + height: 30px; + background-color: var(--color-tertiary); + } + } + } + .course-language { + display: flex; + flex-direction: column; + justify-content: center; + .icon { + color: var(--color-tertiary); + display: flex; + justify-content: center; + align-items: center; + gap: space.$space-2; + font-weight: bold; + padding-bottom: space.$space-8; + &:before { + @include icons.icon-language; + width: 30px; + height: 30px; + background-color: var(--color-tertiary); + } + } + } + .course-pace { + display: flex; + flex-direction: column; + justify-content: center; + .icon { + color: var(--color-tertiary); + display: flex; + justify-content: center; + align-items: center; + gap: space.$space-2; + font-weight: bold; + padding-bottom: space.$space-8; + &:before { + @include icons.icon-schedule; + width: 30px; + height: 30px; + background-color: var(--color-tertiary); + } + } + } + } + .course-link { + @media (max-width: 600px) { + padding-top: space.$space-20; + } + } } } .table-container { @@ -123,5 +211,8 @@ td { vertical-align: middle; } + td:last-child { + white-space: pre-line; + } } } diff --git a/public/js/app/components/NewSearchResultDisplay/types.ts b/public/js/app/components/NewSearchResultDisplay/types.ts index 158e382f..a919a86d 100644 --- a/public/js/app/components/NewSearchResultDisplay/types.ts +++ b/public/js/app/components/NewSearchResultDisplay/types.ts @@ -1,11 +1,11 @@ import { STATUS } from '../../hooks/types/UseCourseSearchTypes' -import { KoppsCourseSearchResult, KoppsCourseSearchResultState } from '../../util/types/SearchApiTypes' +import { CourseSearchResult, CourseSearchResultState } from '../../util/types/SearchApiTypes' import { SearchHits } from '../../util/types/SearchApiTypes' -export { KoppsCourseSearchResult, KoppsCourseSearchResultState } +export { CourseSearchResult, CourseSearchResultState } export interface SearchResultDisplayParams { - resultsState: KoppsCourseSearchResultState + resultsState: CourseSearchResultState } export interface SearchResultHeaderParams { @@ -16,7 +16,7 @@ export interface SearchResultHeaderParams { } export interface SearchResultComponentParams { - searchResults: KoppsCourseSearchResult + searchResults: CourseSearchResult view: View } diff --git a/public/js/app/components/SearchAlert/index.tsx b/public/js/app/components/SearchAlert/index.tsx index 7a877685..7d131189 100644 --- a/public/js/app/components/SearchAlert/index.tsx +++ b/public/js/app/components/SearchAlert/index.tsx @@ -6,7 +6,7 @@ import { SearchAlertProps } from './types' const SearchAlert: React.FC = ({ alertType: externalAlertType, languageIndex }) => { const { searchAlarms } = i18n.messages[languageIndex] - const { header, help, text } = searchAlarms[externalAlertType] + const { header, help, text } = searchAlarms[externalAlertType] || searchAlarms['errorUnknown'] return ( diff --git a/public/js/app/components/SearchFilters/SearchFilters.test.tsx b/public/js/app/components/SearchFilters/SearchFilters.test.tsx index a769194a..8e85cfca 100644 --- a/public/js/app/components/SearchFilters/SearchFilters.test.tsx +++ b/public/js/app/components/SearchFilters/SearchFilters.test.tsx @@ -3,7 +3,7 @@ import { render, screen, fireEvent } from '@testing-library/react' import '@testing-library/jest-dom' import SearchFilters from './index' import { useStore } from '../../mobx' -import { EduLevel, Period, ShowOptions } from '../../stores/types/searchPageStoreTypes' +import { EduLevel, Period, Semester, ShowOptions } from '../../stores/types/searchPageStoreTypes' import { SEARCH_MODES } from '../../pages/types/searchPageTypes' // Mocking the useStore hook @@ -42,7 +42,7 @@ describe('', () => { const courseSearchParams = { pattern: '', - period: [] as Period[], + semesters: [] as Semester[], eduLevel: [] as EduLevel[], showOptions: [] as ShowOptions[], department: '', diff --git a/public/js/app/components/SearchFilters/index.tsx b/public/js/app/components/SearchFilters/index.tsx index a8c16108..d15a2dd1 100644 --- a/public/js/app/components/SearchFilters/index.tsx +++ b/public/js/app/components/SearchFilters/index.tsx @@ -7,7 +7,13 @@ import i18n from '../../../../../i18n' import NewSearchDepartments from '../NewSearchDepartments' import NewSearchOptions from '../NewSearchOptions' import { FilterParams, SearchFilterStore, SearchFiltersProps, FILTER_MODES } from './types' -import { DepartmentCodeOrPrefix, EduLevel, Period, ShowOptions } from '../../stores/types/searchPageStoreTypes' +import { + DepartmentCodeOrPrefix, + EduLevel, + Period, + Semester, + ShowOptions, +} from '../../stores/types/searchPageStoreTypes' import { SEARCH_MODES } from '../../pages/types/searchPageTypes' const SearchFilters: React.FC = ({ disabled, @@ -33,7 +39,7 @@ const SearchFilters: React.FC = ({ function handleClearFilters() { setCourseSearchParams({ - period: [], + semesters: [], eduLevel: searchMode === SEARCH_MODES.thirdCycleCourses ? ['3'] : [], showOptions: [], department: '', @@ -49,24 +55,12 @@ const SearchFilters: React.FC = ({ const renderFilterGroup = ( <> - {filterMode.includes('period') && ( + {filterMode.includes('semesters') && (
-
-
- diff --git a/public/js/app/components/SearchFilters/types.ts b/public/js/app/components/SearchFilters/types.ts index 69fb2d1d..d39b4cd2 100644 --- a/public/js/app/components/SearchFilters/types.ts +++ b/public/js/app/components/SearchFilters/types.ts @@ -5,6 +5,7 @@ import { EduLevel, Period, SearchCoursesStore, + Semester, ShowOptions, } from '../../stores/types/searchPageStoreTypes' @@ -22,15 +23,15 @@ export interface SearchFiltersProps { } export interface FilterParams { - [key: string]: (Period | EduLevel | ShowOptions)[] | DepartmentCodeOrPrefix + [key: string]: (Period | Semester | EduLevel | ShowOptions)[] | DepartmentCodeOrPrefix } export interface SearchFilterStore extends SearchCoursesStore { languageIndex: number } export const FILTER_MODES: Record = { - default: ['period', 'eduLevel', 'showOptions', 'department'], + default: ['semesters', 'eduLevel', 'showOptions', 'department'], thirdCycleCourses: ['onlyMHU', 'department'], } as const -type FilterModeKey = 'period' | 'eduLevel' | 'showOptions' | 'department' | 'onlyMHU' +type FilterModeKey = 'semesters' | 'eduLevel' | 'showOptions' | 'department' | 'onlyMHU' diff --git a/public/js/app/components/mocks/mockCourseSeasrch.ts b/public/js/app/components/mocks/mockCourseSeasrch.ts new file mode 100644 index 00000000..e69f3537 --- /dev/null +++ b/public/js/app/components/mocks/mockCourseSeasrch.ts @@ -0,0 +1,300 @@ +import { ErrorAsync } from '../../hooks/types/UseCourseSearchTypes' +import { SearchHits } from '../../util/types/SearchApiTypes' + +const TEST_API_ANSWER_OVERFLOW = { + searchHits: [] as SearchHits[], + errorCode: 'search-error-overflow' as ErrorAsync, + errorMessage: 'search-error-overflow', +} + +const TEST_API_ANSWER_NO_HITS = { + searchHits: [] as SearchHits[], +} + +const TEST_API_ANSWER_UNKNOWN_ERROR = { + searchHits: [] as SearchHits[], + errorCode: 'search-error-unknown' as ErrorAsync, + errorMessage: 'search-error-unknown', +} + +const TEST_API_ANSWER_RESOLVED = { + searchHits: [ + { + kod: 'AF2402', + benamning: 'Acoustics and Fire', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Engineering', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Basic level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'BASIC', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2022-01-17', + period: 3, + year: 2022, + week: 3, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'AH2905', + benamning: 'Advanced Pavement Engineering Analysis and Design', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Engineering', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, First-cycle', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'First cycle', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2021-12-17', + period: 1, + year: 2021, + week: 50, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + ], +} + +const TEST_API_ANSWER_EMPTY_PARAMS = 'No query restriction was specified' +const TEST_API_ANSWER_ALGEBRA = { + searchHits: [ + { + kod: 'IX1303', + benamning: 'Algebra och geometri', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Mathematics', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Basic level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'BASIC', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'SF1624', + benamning: 'Algebra och geometri', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Mathematics', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Basic level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'BASIC', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + schoolCode: 'SCI', + }, + ], +} + +export { + TEST_API_ANSWER_ALGEBRA, + TEST_API_ANSWER_EMPTY_PARAMS, + TEST_API_ANSWER_UNKNOWN_ERROR, + TEST_API_ANSWER_OVERFLOW, + TEST_API_ANSWER_NO_HITS, + TEST_API_ANSWER_RESOLVED, +} diff --git a/public/js/app/components/mocks/mockSearchHits.ts b/public/js/app/components/mocks/mockSearchHits.ts index 9fd35fb0..6ef84534 100644 --- a/public/js/app/components/mocks/mockSearchHits.ts +++ b/public/js/app/components/mocks/mockSearchHits.ts @@ -82,6 +82,402 @@ const TEST_SEARCH_HITS_MIXED_EN = { }, ], } +const TEST_SEARCH_HITS_MIXED_EN_BETA = { + searchHits: [ + { + kod: 'AF2402', + benamning: 'Acoustics and Fire', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Engineering', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Basic level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'BASIC', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2022-01-17', + period: 3, + year: 2022, + week: 3, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'AH2905', + benamning: 'Advanced Pavement Engineering Analysis and Design', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Engineering', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, First-cycle', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'First cycle', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2021-12-17', + period: 1, + year: 2021, + week: 50, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'FAF3901', + benamning: 'Advanced Rheology of Bituminous Materials', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Research-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'RESEARCH', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-09-01', + week: 36, + }, + sistaUndervisningsdatum: { + date: '2021-12-15', + week: 50, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'AF233X', + benamning: 'Degree Project in Building Materials, Second Cycle', + omfattning: { + formattedWithUnit: '30 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Advanced-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'ADVANCED', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '100', + name: 'Full-time', + takt: 100, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2021-12-15', + period: 2, + year: 2021, + week: 50, + }, + tillfallesperioderNummer: 2, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'FAH3904', + benamning: 'Introduction to Asphalt Chemistry', + omfattning: { + formattedWithUnit: '4 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Preparatory-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '0', + name: 'PREPARATORY', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 0, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2022-05-15', + period: 5, + year: 2022, + week: 20, + }, + tillfallesperioderNummer: 5, + }, + ], + schoolCode: 'SCI', + }, + ], +} + const TEST_SEARCH_HITS_MIXED_SV = { // UNSORTED searchHits: [ @@ -165,6 +561,395 @@ const TEST_SEARCH_HITS_MIXED_SV = { }, ], } + +const TEST_SEARCH_HITS_MIXED_SV_BETA = { + searchHits: [ + { + kod: 'AF2402', + benamning: 'Akustik och brand', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Engineering', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Basic level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'BASIC', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2022-01-17', + period: 3, + year: 2022, + week: 3, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'AH2905', + benamning: 'Avancerad analys och design av vägbeläggningar', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Engineering', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, First-cycle', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'First cycle', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2021-12-17', + period: 1, + year: 2021, + week: 50, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'FAF3901', + benamning: 'Avancerad reologi för bituminösa material', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Research-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'RESEARCH', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'AF233X', + benamning: 'Examensarbete inom byggnadsmateriallära, avancerad nivå', + omfattning: { + formattedWithUnit: '30 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Advanced-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'ADVANCED', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '100', + name: 'Full-time', + takt: 100, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2021-12-15', + period: 2, + year: 2021, + week: 50, + }, + tillfallesperioderNummer: 2, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'FAH3904', + benamning: 'Introduktion till asfaltskemin', + omfattning: { + formattedWithUnit: '4 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Preparatory-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '0', + name: 'PREPARATORY', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 0, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2022-05-15', + period: 5, + year: 2022, + week: 20, + }, + tillfallesperioderNummer: 5, + }, + ], + schoolCode: 'SCI', + }, + ], +} + const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN = [ 'P1 Autumn 21 - P2 Autumn 21', 'P1 Autumn 21 - P3 Spring 22', @@ -173,7 +958,7 @@ const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN = [ 'P0 Autumn 21 - P5 Spring 22', ] // todo: this needs to be deleted after removing the old search -const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN_new = [ +const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN_BETA = [ 'P1 Autumn 21 - P2 Autumn 21', 'P1 Autumn 21 - P3 Spring 22', 'P1 Autumn 21', @@ -188,7 +973,7 @@ const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV = [ 'P0 HT21 - P5 VT22', ] -const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV_new = [ +const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV_BETA = [ 'P1 HT21 - P2 HT21', 'P1 HT21 - P3 VT22', 'P1 HT21', @@ -280,6 +1065,395 @@ const EXPECTED_TEST_SEARCH_HITS_MIXED_EN = { }, ], } + +const EXPECTED_TEST_SEARCH_HITS_MIXED_EN_BETA = { + searchHits: [ + { + kod: 'AF233X', + benamning: 'Degree Project in Building Materials, Second Cycle', + omfattning: { + formattedWithUnit: '30 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Advanced-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'ADVANCED', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '100', + name: 'Full-time', + takt: 100, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2021-12-15', + period: 2, + year: 2021, + week: 50, + }, + tillfallesperioderNummer: 2, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'AF2402', + benamning: 'Acoustics and Fire', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Engineering', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Basic level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'BASIC', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2022-01-17', + period: 3, + year: 2022, + week: 3, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'AH2905', + benamning: 'Advanced Pavement Engineering Analysis and Design', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Engineering', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, First-cycle', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'First cycle', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2021-12-17', + period: 1, + year: 2021, + week: 50, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'FAF3901', + benamning: 'Advanced Rheology of Bituminous Materials', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Research-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'RESEARCH', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'FAH3904', + benamning: 'Introduction to Asphalt Chemistry', + omfattning: { + formattedWithUnit: '4 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Preparatory-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '0', + name: 'PREPARATORY', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 0, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2022-05-15', + period: 5, + year: 2022, + week: 20, + }, + tillfallesperioderNummer: 5, + }, + ], + schoolCode: 'SCI', + }, + ], +} + const EXPECTED_TEST_SEARCH_HITS_MIXED_SV = { // SORTED searchHits: [ @@ -365,6 +1539,394 @@ const EXPECTED_TEST_SEARCH_HITS_MIXED_SV = { ], } +const EXPECTED_TEST_SEARCH_HITS_MIXED_SV_BETA = { + searchHits: [ + { + kod: 'AF233X', + benamning: 'Examensarbete inom byggnadsmateriallära, avancerad nivå', + omfattning: { + formattedWithUnit: '30 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Advanced-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'ADVANCED', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '100', + name: 'Full-time', + takt: 100, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2021-12-15', + period: 2, + year: 2021, + week: 50, + }, + tillfallesperioderNummer: 2, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'AF2402', + benamning: 'Akustik och brand', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Engineering', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Basic level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'BASIC', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2022-01-17', + period: 3, + year: 2022, + week: 3, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'AH2905', + benamning: 'Avancerad analys och design av vägbeläggningar', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Engineering', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, First-cycle', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'First cycle', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2021-12-17', + period: 1, + year: 2021, + week: 50, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'FAF3901', + benamning: 'Avancerad reologi för bituminösa material', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Research-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'RESEARCH', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'FAH3904', + benamning: 'Introduktion till asfaltskemin', + omfattning: { + formattedWithUnit: '4 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Preparatory-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '0', + name: 'PREPARATORY', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 0, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2022-05-15', + period: 5, + year: 2022, + week: 20, + }, + tillfallesperioderNummer: 5, + }, + ], + schoolCode: 'SCI', + }, + ], +} + const TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV = { // RESEARCH, NO searchHitInterval searchHits: [ @@ -401,6 +1963,167 @@ const TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV = { ], } +const TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV_BETA = { + searchHits: [ + { + kod: 'FAF3302', + benamning: 'Projekt i byggnadsmaterialteknik', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Research-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'Forskarnivå', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'FAF3304', + benamning: 'Träkemi för biokompositer som byggnadsmaterial', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Research-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'Forskarnivå', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'FAF3305', + benamning: 'Vägdimensionering och prestandautvärdering', + omfattning: { + formattedWithUnit: '3 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Research-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'Forskarnivå', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + schoolCode: 'SCI', + }, + ], +} + const TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN = { // RESEARCH, NO searchHitInterval searchHits: [ @@ -437,15 +2160,182 @@ const TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN = { ], } +const TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN_BETA = { + searchHits: [ + { + kod: 'FAF3302', + benamning: 'Project in Building Materials Technology', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Research-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'Third cycle', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'FAF3304', + benamning: 'Wood Chemistry, Biocomposites and Building Materials', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Research-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'Third cycle', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'FAF3305', + benamning: 'Pavement Design and Performance Prediction', + omfattning: { + formattedWithUnit: '3 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Materials Science', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Research-level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '2', + name: 'Third cycle', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + schoolCode: 'SCI', + }, + ], +} + export { EXPECTED_TEST_SEARCH_HITS_MIXED_EN, + EXPECTED_TEST_SEARCH_HITS_MIXED_EN_BETA, EXPECTED_TEST_SEARCH_HITS_MIXED_SV, + EXPECTED_TEST_SEARCH_HITS_MIXED_SV_BETA, EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN, - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN_new, + EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN_BETA, EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV, - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV_new, + EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV_BETA, TEST_SEARCH_HITS_MIXED_EN, + TEST_SEARCH_HITS_MIXED_EN_BETA, TEST_SEARCH_HITS_MIXED_SV, + TEST_SEARCH_HITS_MIXED_SV_BETA, TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV, + TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV_BETA, TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN, + TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN_BETA, } diff --git a/public/js/app/hooks/tests/useLangHrefUpdates.test.tsx b/public/js/app/hooks/tests/useLangHrefUpdates.test.tsx index 34d5c041..b2206b8a 100644 --- a/public/js/app/hooks/tests/useLangHrefUpdates.test.tsx +++ b/public/js/app/hooks/tests/useLangHrefUpdates.test.tsx @@ -48,7 +48,7 @@ describe('useLangHrefUpdate', () => { const courseSearchParams = { pattern: 'test', department: 'A', - period: ['20242:1', '20242:2', '2025:summer', '20251:3', '20251:4', '20252:1', '20252:2'], + semesters: ['HT2024', 'VT2025', 'HT2025'], eduLevel: ['0', '1', '2', '3'], showOptions: ['onlyEnglish', 'onlyMHU', 'showCancelled'], } @@ -56,7 +56,7 @@ describe('useLangHrefUpdate', () => { render() expect(mockLanguageAnchor?.href).toContain( - '?pattern=test&department=A&period=20242%3A1&period=20242%3A2&period=2025%3Asummer&period=20251%3A3&period=20251%3A4&period=20252%3A1&period=20252%3A2&eduLevel=0&eduLevel=1&eduLevel=2&eduLevel=3&showOptions=onlyEnglish&showOptions=onlyMHU&showOptions=showCancelled&l=se' + '?pattern=test&department=A&semesters=HT2024&semesters=VT2025&semesters=HT2025&eduLevel=0&eduLevel=1&eduLevel=2&eduLevel=3&showOptions=onlyEnglish&showOptions=onlyMHU&showOptions=showCancelled&l=se' ) }) @@ -65,7 +65,7 @@ describe('useLangHrefUpdate', () => { pattern: '', eduLevel: ['0'], department: undefined as any, - period: [] as any, + semesters: [] as any, } render() diff --git a/public/js/app/hooks/useCourseSearch.ts b/public/js/app/hooks/useCourseSearch.ts index 24a2922f..6b5bf3e2 100644 --- a/public/js/app/hooks/useCourseSearch.ts +++ b/public/js/app/hooks/useCourseSearch.ts @@ -42,7 +42,7 @@ function useCourseSearch(asyncCallback: () => Promise, initialState?: Part if (errorCode && errorCode === 'search-error-overflow') overflowDispatch(dispatch) else if (errorCode) dispatch({ type: 'rejected', error: errorCode }) else if (searchHits && searchHits.length === 0) noHitsDispatch(dispatch) - else if (!searchHits && typeof data === 'string' && data.includes('ERROR-koppsCourseSearch-')) + else if (!searchHits && typeof data === 'string' && data.includes('ERROR-courseSearch-')) dispatch({ type: 'rejected', error: data }) else if (!searchHits || data === 'No query restriction was specified') noQueryProvidedDispatch(dispatch) else dispatch({ type: 'resolved', data }) diff --git a/public/js/app/hooks/useCourseSearchParams.ts b/public/js/app/hooks/useCourseSearchParams.ts index 32cd3285..2e15dd3f 100644 --- a/public/js/app/hooks/useCourseSearchParams.ts +++ b/public/js/app/hooks/useCourseSearchParams.ts @@ -1,7 +1,7 @@ import { useSearchParams } from 'react-router-dom' import { CourseSearchParams, SetCourseSearchParams } from '../pages/types/searchPageTypes' import { useMemo } from 'react' -import { EduLevel, Period, ShowOptions, Pattern, DepartmentCodeOrPrefix } from '../stores/types/searchPageStoreTypes' +import { EduLevel, Period, ShowOptions, Pattern, DepartmentCodeOrPrefix, Semester } from '../stores/types/searchPageStoreTypes' /** * Wrapper hooks around useSearchParams that handles conversion between url state (URLSearchParams) @@ -12,7 +12,7 @@ export const useCourseSearchParams = (): [CourseSearchParams, SetCourseSearchPar const courseSearchParams: CourseSearchParams = useMemo( () => ({ pattern: searchParams.get('pattern') as Pattern ?? '', - period: (searchParams.getAll('period').filter(Boolean) as Period[]), + semesters: (searchParams.getAll('semesters').filter(Boolean) as Semester[]), eduLevel: (searchParams.getAll('eduLevel').filter(Boolean) as EduLevel[]), showOptions: (searchParams.getAll('showOptions').filter(Boolean) as ShowOptions[]), department: searchParams.get('department') as DepartmentCodeOrPrefix ?? '', diff --git a/public/js/app/pages/NewSearchLandingPage.tsx b/public/js/app/pages/NewSearchLandingPage.tsx index 48e9d21b..c54b3ac4 100644 --- a/public/js/app/pages/NewSearchLandingPage.tsx +++ b/public/js/app/pages/NewSearchLandingPage.tsx @@ -40,7 +40,7 @@ const NewSearchLandingPage: React.FC = ({ searchMode = SEARCH_M const [courseSearchParams, setCourseSearchParams] = useReducer(paramsReducer, { pattern: '', - period: [], + semesters: [], eduLevel: [], showOptions: [], department: '', diff --git a/public/js/app/pages/NewSearchPage.tsx b/public/js/app/pages/NewSearchPage.tsx index 525a714c..500a4a7d 100644 --- a/public/js/app/pages/NewSearchPage.tsx +++ b/public/js/app/pages/NewSearchPage.tsx @@ -6,7 +6,7 @@ import { SearchFilters } from '../components' import SearchInput from '../components/SearchInput' -import { koppsCourseSearch } from '../util/searchApi' +import { courseSearch } from '../util/searchApi' import { useCourseSearch } from '../hooks/useCourseSearch' import { useStore } from '../mobx' @@ -16,7 +16,7 @@ import { MainContentProps, SEARCH_MODES, SearchPageProps } from './types/searchP import { useCourseSearchParams } from '../hooks/useCourseSearchParams' import { STATUS } from '../hooks/types/UseCourseSearchTypes' import NewSearchResultDisplay from '../components/NewSearchResultDisplay' -import { KoppsCourseSearchResultState } from '../util/types/SearchApiTypes' +import { CourseSearchResultState } from '../util/types/SearchApiTypes' import { useLangHrefUpdate } from '../hooks/useLangHrefUpdate' import { FILTER_MODES } from '../components/SearchFilters/types' import { SidebarFilters } from '../components/SidebarFilters' @@ -46,7 +46,7 @@ const NewSearchPage: React.FC = ({ searchMode = SEARCH_MODES.de const asyncCallback = React.useCallback(() => { const proxyUrl = _getThisHost(browserConfig.proxyPrefixPath.uri) - return koppsCourseSearch(language, proxyUrl, courseSearchParams) + return courseSearch(language, proxyUrl, courseSearchParams) }, [courseSearchParams]) const state = useCourseSearch(asyncCallback, { status: STATUS.idle }) @@ -88,7 +88,7 @@ const NewSearchPage: React.FC = ({ searchMode = SEARCH_MODES.de searchLabel={searchLabel} disabled={searchStatus === STATUS.pending} /> - + ) diff --git a/public/js/app/pages/SearchLandingPage.test.tsx b/public/js/app/pages/SearchLandingPage.test.tsx index 4cf93639..ec1c3b7c 100644 --- a/public/js/app/pages/SearchLandingPage.test.tsx +++ b/public/js/app/pages/SearchLandingPage.test.tsx @@ -15,15 +15,7 @@ jest.mock('react-router-dom', () => ({ useNavigate: jest.fn(), useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]), })) -const periods = [ - 'Autumn 2024 period 1', - 'Autumn 2024 period 2', - '2025 summer', - 'Spring 2025 period 3', - 'Spring 2025 period 4', - 'Autumn 2025 period 1', - 'Autumn 2025 period 2', -] +const periods = ['Autumn 2024', 'Spring 2025', 'Autumn 2025'] const eduLevels = ['Pre-university level', 'First cycle', 'Second cycle', 'Third cycle'] const showOptions = [ 'Courses taught in English', @@ -89,7 +81,7 @@ describe('', () => { fireEvent.click(button) const searchParams = stringifyUrlParams({ - period: ['20242:1', '20242:2', '2025:summer', '20251:3', '20251:4', '20252:1', '20252:2'], + semesters: ['HT2024', 'VT2025', 'HT2025'], eduLevel: ['0', '1', '2', '3'], showOptions: ['onlyEnglish', 'onlyMHU', 'showCancelled'], }) @@ -115,7 +107,7 @@ describe('', () => { fireEvent.click(button) const searchParams = stringifyUrlParams({ - period: [], + semesters: [], eduLevel: [], showOptions: [], }) @@ -162,7 +154,7 @@ describe('', () => { fireEvent.click(button) const searchParams = stringifyUrlParams({ - period: ['20242:1'], + semesters: ['HT2024'], eduLevel: ['0'], showOptions: ['onlyEnglish'], }) diff --git a/public/js/app/pages/SearchPage.test.tsx b/public/js/app/pages/SearchPage.test.tsx index e27ae744..b26235b7 100644 --- a/public/js/app/pages/SearchPage.test.tsx +++ b/public/js/app/pages/SearchPage.test.tsx @@ -3,18 +3,10 @@ import { render, screen, fireEvent, waitFor, waitForElementToBeRemoved } from '@ import '@testing-library/jest-dom' import NewSearchPage from './NewSearchPage' import { useStore } from '../mobx' -import { koppsCourseSearch } from '../util/searchApi' +import { courseSearch } from '../util/searchApi' import { TEST_API_ANSWER_RESOLVED } from '../components/mocks/mockKoppsCourseSearch' -const periods = [ - 'Autumn 2024 period 1', - 'Autumn 2024 period 2', - '2025 summer', - 'Spring 2025 period 3', - 'Spring 2025 period 4', - 'Autumn 2025 period 1', - 'Autumn 2025 period 2', -] +const periods = ['Autumn 2024', 'Spring 2025', 'Autumn 2025'] const eduLevels = ['Pre-university level', 'First cycle', 'Second cycle', 'Third cycle'] const showOptions = [ 'Courses taught in English', @@ -72,19 +64,19 @@ describe('', () => { test('should load search parameters from URL and call search API', async () => { mockSearchParams = new URLSearchParams() - // Add multiple values for the 'period' key + // Add multiple values for the 'semesters' key mockSearchParams.append('pattern', 'Math') - mockSearchParams.append('period', '20242:1') - mockSearchParams.append('period', '20242:2') + mockSearchParams.append('semesters', 'HT2024') + mockSearchParams.append('semesters', 'VT2024') mockSearchParams.append('eduLevel', '1') mockSearchParams.append('showOptions', 'onlyEnglish') - ;(koppsCourseSearch as jest.Mock).mockReturnValue(Promise.resolve(TEST_API_ANSWER_RESOLVED)) + ;(courseSearch as jest.Mock).mockReturnValue(Promise.resolve(TEST_API_ANSWER_RESOLVED)) render() - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: 'Math', - period: ['20242:1', '20242:2'], + semesters: ['HT2024', 'VT2024'], eduLevel: ['1'], showOptions: ['onlyEnglish'], department: '', @@ -92,12 +84,9 @@ describe('', () => { expect(screen.getByRole('textbox')).toHaveValue('Math') - const periodCheckbox = screen.getByLabelText('Autumn 2024 period 1') + const periodCheckbox = screen.getByLabelText('Autumn 2024') expect(periodCheckbox).toBeChecked() - const secondPeriodCheckbox = screen.getByLabelText('Autumn 2024 period 2') - expect(secondPeriodCheckbox).toBeChecked() - const eduLevelCheckbox = screen.getByLabelText('First cycle') expect(eduLevelCheckbox).toBeChecked() }) @@ -106,7 +95,7 @@ describe('', () => { mockSearchParams = new URLSearchParams({ pattern: '', }) - ;(koppsCourseSearch as jest.Mock).mockReturnValue(Promise.resolve(TEST_API_ANSWER_RESOLVED)) + ;(courseSearch as jest.Mock).mockReturnValue(Promise.resolve(TEST_API_ANSWER_RESOLVED)) render() @@ -116,11 +105,11 @@ describe('', () => { await waitFor(() => { expect(mockSearchParams.get('pattern')).toBe('Physics') - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: 'Physics', department: '', eduLevel: [], - period: [], + semesters: [], showOptions: [], }) }) @@ -139,8 +128,8 @@ describe('', () => { const searchInput = screen.getByRole('textbox') expect(searchInput).toBeDisabled() - periods.forEach(period => { - const periodCheckbox = screen.getByLabelText(period) + periods.forEach(semesters => { + const periodCheckbox = screen.getByLabelText(semesters) expect(periodCheckbox).toBeDisabled() }) @@ -155,29 +144,29 @@ describe('', () => { }) }) - test('should update search params and call search API when period checkboxes, eduLevel, and showOptions are changed', async () => { - ;(koppsCourseSearch as jest.Mock).mockReturnValue(Promise.resolve(TEST_API_ANSWER_RESOLVED)) + test('should update search params and call search API when semesters checkboxes, eduLevel, and showOptions are changed', async () => { + ;(courseSearch as jest.Mock).mockReturnValue(Promise.resolve(TEST_API_ANSWER_RESOLVED)) render() // Verify that the initial API call was made with the correct parameters - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: '', department: '', eduLevel: [], - period: [], + semesters: [], showOptions: [], }) const firstPeriodCheckbox = screen.getByLabelText(periods[0]) expect(firstPeriodCheckbox).not.toBeChecked() fireEvent.click(firstPeriodCheckbox) await waitFor(async () => { - expect(mockSearchParams.getAll('period')).toEqual(['20242:1']) - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(mockSearchParams.getAll('semesters')).toEqual(['HT2024']) + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: '', department: '', eduLevel: [], - period: ['20242:1'], + semesters: ['HT2024'], showOptions: [], }) @@ -187,11 +176,11 @@ describe('', () => { }) await waitFor(async () => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: '', department: '', eduLevel: [], - period: ['20242:1', '20242:2'], + semesters: ['HT2024', 'VT2025'], showOptions: [], }) @@ -201,67 +190,11 @@ describe('', () => { }) await waitFor(async () => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { - pattern: '', - department: '', - eduLevel: [], - period: ['20242:1', '20242:2', '2025:summer'], - showOptions: [], - }) - - const forthPeriodCheckbox = screen.getByLabelText(periods[3]) - expect(forthPeriodCheckbox).not.toBeChecked() - fireEvent.click(forthPeriodCheckbox) - }) - - await waitFor(async () => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { - pattern: '', - department: '', - eduLevel: [], - period: ['20242:1', '20242:2', '2025:summer', '20251:3'], - showOptions: [], - }) - - const fifthPeriodCheckbox = screen.getByLabelText(periods[4]) - expect(fifthPeriodCheckbox).not.toBeChecked() - fireEvent.click(fifthPeriodCheckbox) - }) - - await waitFor(async () => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { - pattern: '', - department: '', - eduLevel: [], - period: ['20242:1', '20242:2', '2025:summer', '20251:3', '20251:4'], - showOptions: [], - }) - - const sixthPeriodCheckbox = screen.getByLabelText(periods[5]) - expect(sixthPeriodCheckbox).not.toBeChecked() - fireEvent.click(sixthPeriodCheckbox) - }) - - await waitFor(async () => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { - pattern: '', - department: '', - eduLevel: [], - period: ['20242:1', '20242:2', '2025:summer', '20251:3', '20251:4', '20252:1'], - showOptions: [], - }) - - const seventhPeriodCheckbox = screen.getByLabelText(periods[6]) - expect(seventhPeriodCheckbox).not.toBeChecked() - fireEvent.click(seventhPeriodCheckbox) - }) - - await waitFor(async () => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: '', department: '', eduLevel: [], - period: ['20242:1', '20242:2', '2025:summer', '20251:3', '20251:4', '20252:1', '20252:2'], + semesters: ['HT2024', 'VT2025', 'HT2025'], showOptions: [], }) @@ -271,11 +204,11 @@ describe('', () => { }) await waitFor(async () => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: '', department: '', eduLevel: ['0'], - period: ['20242:1', '20242:2', '2025:summer', '20251:3', '20251:4', '20252:1', '20252:2'], + semesters: ['HT2024', 'VT2025', 'HT2025'], showOptions: [], }) @@ -285,11 +218,11 @@ describe('', () => { }) await waitFor(async () => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: '', department: '', eduLevel: ['0', '1'], - period: ['20242:1', '20242:2', '2025:summer', '20251:3', '20251:4', '20252:1', '20252:2'], + semesters: ['HT2024', 'VT2025', 'HT2025'], showOptions: [], }) @@ -299,11 +232,11 @@ describe('', () => { }) await waitFor(async () => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: '', department: '', eduLevel: ['0', '1', '2'], - period: ['20242:1', '20242:2', '2025:summer', '20251:3', '20251:4', '20252:1', '20252:2'], + semesters: ['HT2024', 'VT2025', 'HT2025'], showOptions: [], }) @@ -313,11 +246,11 @@ describe('', () => { }) await waitFor(async () => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: '', department: '', eduLevel: ['0', '1', '2', '3'], - period: ['20242:1', '20242:2', '2025:summer', '20251:3', '20251:4', '20252:1', '20252:2'], + semesters: ['HT2024', 'VT2025', 'HT2025'], showOptions: [], }) @@ -327,11 +260,11 @@ describe('', () => { }) await waitFor(async () => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: '', department: '', eduLevel: ['0', '1', '2', '3'], - period: ['20242:1', '20242:2', '2025:summer', '20251:3', '20251:4', '20252:1', '20252:2'], + semesters: ['HT2024', 'VT2025', 'HT2025'], showOptions: ['onlyEnglish'], }) @@ -341,11 +274,11 @@ describe('', () => { }) await waitFor(async () => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: '', department: '', eduLevel: ['0', '1', '2', '3'], - period: ['20242:1', '20242:2', '2025:summer', '20251:3', '20251:4', '20252:1', '20252:2'], + semesters: ['HT2024', 'VT2025', 'HT2025'], showOptions: ['onlyEnglish', 'onlyMHU'], }) @@ -355,11 +288,11 @@ describe('', () => { }) await waitFor(() => { - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { + expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: '', department: '', eduLevel: ['0', '1', '2', '3'], - period: ['20242:1', '20242:2', '2025:summer', '20251:3', '20251:4', '20252:1', '20252:2'], + semesters: ['HT2024', 'VT2025', 'HT2025'], showOptions: ['onlyEnglish', 'onlyMHU', 'showCancelled'], }) }) diff --git a/public/js/app/pages/mocks/Appendix1ApplicationStore.js b/public/js/app/pages/mocks/Appendix1ApplicationStore.js index 1d2b546d..c2dcc98b 100644 --- a/public/js/app/pages/mocks/Appendix1ApplicationStore.js +++ b/public/js/app/pages/mocks/Appendix1ApplicationStore.js @@ -4,7 +4,9 @@ const applicationStores = [ proxyPrefixPath: { uri: '/student/kurser', courseSearch: '/student/kurser/sokkurs', + newSearchPage: '/student/kurser/sokkurs-beta', courseSearchInternApi: '/student/kurser/intern-api/sok', + courseSearchInternApiBeta: '/student/kurser/intern-api/sokBeta', department: '/student/kurser/org', programme: '/student/kurser/program', programmesList: '/student/kurser/kurser-inom-program', @@ -520,7 +522,9 @@ const applicationStores = [ proxyPrefixPath: { uri: '/student/kurser', courseSearch: '/student/kurser/sokkurs', + newSearchPage: '/student/kurser/sokkurs-beta', courseSearchInternApi: '/student/kurser/intern-api/sok', + courseSearchInternApiBeta: '/student/kurser/intern-api/sokBeta', department: '/student/kurser/org', programme: '/student/kurser/program', programmesList: '/student/kurser/kurser-inom-program', @@ -3504,7 +3508,9 @@ const applicationStores = [ proxyPrefixPath: { uri: '/student/kurser', courseSearch: '/student/kurser/sokkurs', + newSearchPage: '/student/kurser/sokkurs-beta', courseSearchInternApi: '/student/kurser/intern-api/sok', + courseSearchInternApiBeta: '/student/kurser/intern-api/sokBeta', department: '/student/kurser/org', programme: '/student/kurser/program', programmesList: '/student/kurser/kurser-inom-program', diff --git a/public/js/app/pages/types/searchPageTypes.ts b/public/js/app/pages/types/searchPageTypes.ts index 6ee6a1e9..f96abbe6 100644 --- a/public/js/app/pages/types/searchPageTypes.ts +++ b/public/js/app/pages/types/searchPageTypes.ts @@ -1,6 +1,13 @@ import React from 'react' -import { Pattern, Period, EduLevel, ShowOptions, DepartmentCodeOrPrefix } from '../../stores/types/searchPageStoreTypes' +import { + Pattern, + Period, + EduLevel, + ShowOptions, + DepartmentCodeOrPrefix, + Semester, +} from '../../stores/types/searchPageStoreTypes' export interface MainContentProps { children: React.ReactNode @@ -10,7 +17,7 @@ export type SetCourseSearchParams = (params: Partial) => voi export interface CourseSearchParams { pattern: Pattern - period: Period[] + semesters: Semester[] eduLevel: EduLevel[] showOptions: ShowOptions[] department: DepartmentCodeOrPrefix diff --git a/public/js/app/stores/types/searchPageStoreTypes.ts b/public/js/app/stores/types/searchPageStoreTypes.ts index 202696e2..cb87adbe 100644 --- a/public/js/app/stores/types/searchPageStoreTypes.ts +++ b/public/js/app/stores/types/searchPageStoreTypes.ts @@ -19,6 +19,8 @@ export type SetEduLevels = (eduLevels: EduLevel[]) => void export type Period = `${number}:${'1' | '2' | '3' | '4' | 'summer'}` +export type Semester = `${'VT' | 'HT'}${number}}` + export type SetPeriods = (periods: Period[]) => void export type ShowOptions = 'onlyEnglish' | 'onlyMHU' | 'showCancelled' diff --git a/public/js/app/util/internApi.js b/public/js/app/util/internApi.js index 6b73b6f4..affbeb89 100644 --- a/public/js/app/util/internApi.js +++ b/public/js/app/util/internApi.js @@ -8,14 +8,14 @@ async function koppsCourseSearch(language, proxyUrl, params) { }) if (result) { if (result.status >= 400) { - return 'ERROR-koppsCourseSearch-' + result.status + return 'ERROR-courseSearch-' + result.status } const { data } = result return data } } catch (error) { if (error.response) { - throw new Error('Unexpected error from koppsCourseSearch-' + error.message) + throw new Error('Unexpected error from courseSearch-' + error.message) } throw error } diff --git a/public/js/app/util/newSearchHelper.ts b/public/js/app/util/newSearchHelper.ts index 1d51fbaa..79179c93 100644 --- a/public/js/app/util/newSearchHelper.ts +++ b/public/js/app/util/newSearchHelper.ts @@ -12,7 +12,7 @@ import { import { Link } from '@kth/kth-reactstrap/dist/components/studinfo' import { courseLink } from './links' import React from 'react' -import { formatShortTerm } from '../../../../domain/term' +import { formatShortTerm, formatTermByYearAndPeriod } from '../../../../domain/term' export const getHelpText: GetHelpText = (langIndex, nameOfInstruction, instructionKeys) => { /** @@ -85,51 +85,152 @@ export const compareCoursesBy = (key: T) => { } } -export const periodsStr: PeriodsStrType = (startPeriod, startTerm, endPeriod, endTerm, language) => { - // Ensure startPeriod and endPeriod are strings - const startPeriodStr = startPeriod?.toString() - const endPeriodStr = endPeriod?.toString() - - if (!startTerm || !startPeriodStr) return '' - if (!endTerm || !endPeriodStr) return `P${startPeriod} ${formatShortTerm(startTerm, language)}` - if (startPeriod === endPeriod && startTerm === endTerm) - return `P${startPeriod} ${formatShortTerm(startTerm, language)}` +export const periodsStr: PeriodsStrType = ( + startPeriod, + startPeriodYear, + endPeriod, + endPeriodYear, + tillfallesperioderNummer, + language +) => { + if (!startPeriod && startPeriod !== 0) return '' + if (startPeriod === endPeriod && tillfallesperioderNummer === 1) + return `P${startPeriod} ${formatTermByYearAndPeriod(startPeriod, startPeriodYear, language)}` - return `P${startPeriod} ${formatShortTerm(startTerm, language)} - P${endPeriod} ${formatShortTerm(endTerm, language)}` + return `P${startPeriod} ${formatTermByYearAndPeriod(startPeriod, startPeriodYear, language)} - P${endPeriod} ${formatTermByYearAndPeriod(endPeriod, endPeriodYear, language)}` } -export const sortAndParseByCourseCodeForTableView: SortAndParseByCourseCodeForTableViewType = ( - courses, - sliceUntilNum, - language -) => { - const { bigSearch, generalSearch } = i18n.messages[language === 'en' ? '0' : '1'] +export const sortAndParseByCourseCodeForTableView: SortAndParseByCourseCodeForTableViewType = (courses, language) => { + const { generalSearch } = i18n.messages[language === 'en' ? '0' : '1'] const { courseHasNoRoundsInTableCell } = generalSearch // Sort courses by courseCode - courses.sort(compareCoursesBy('courseCode')) + courses.sort(compareCoursesBy('kod')) // Map and parse courses into the desired format - const parsedCourses = courses.map( - ({ - courseCode: code, - title, - credits, - creditUnitAbbr, - educationalLevel: level, - startPeriod, + const parsedCourses = courses.map(course => { + const { + kod: courseCode, + benamning: title, + utbildningstyp: utbildningstyps = [], + omfattning: { formattedWithUnit: credits = '' } = {}, + period: periods = [], + startperiod: startPeriods = [], + studietakt: studyPaces = [], + undervisningssprak: languages = [], + studieort: campuses = [], + } = course || {} + + const allPeriods = periods.map( + ({ + startperiod: { inDigits: startTerm = '' } = {}, + forstaUndervisningsdatum: { + date: startDate = '', + year: startPeriodYear = '', + week: startWeek = '', + period: startPeriod = '', + } = {}, + sistaUndervisningsdatum: { + date: endDate = '', + year: endPeriodYear = '', + week: endWeek = '', + period: endPeriod = '', + } = {}, + tillfallesperioderNummer = undefined, + }) => ({ + startTerm, + startDate, + startPeriodYear, + startWeek, + startPeriod, + endDate, + endPeriodYear, + endWeek, + endPeriod, + tillfallesperioderNummer, + }) + ) + + const allEducationalLevels = utbildningstyps.map(({ level: { name: educationalLevel = '' } = {} }) => ({ + educationalLevel, + })) + + const allStudyPaces = studyPaces.map(({ takt: coursePace = '' }) => ({ + coursePace, + })) + + const allLanguages = languages.map(({ name: courseLanguage = '' }) => ({ + courseLanguage, + })) + + const allCampuses = campuses.map(({ name: courseCampus = '' }) => ({ + courseCampus, + })) + + const allStartPeriods = startPeriods.map(({ code: startTerm = '', inDigits = '' }) => ({ startTerm, - endPeriod, - endTerm, - }) => - [ - codeCell(code, startTerm, language), - titleCell(code, title, startTerm, language), - `${credits} ${creditUnitAbbr}`, - bigSearch[level] || '', - periodsStr(startPeriod, startTerm, endPeriod, endTerm, language) || courseHasNoRoundsInTableCell, - ].slice(0, sliceUntilNum) - ) + inDigits, + })) + + const startTerm = allStartPeriods.length === 1 ? allStartPeriods[0].startTerm : undefined + + let allPeriodTexts = [] + allPeriodTexts = allPeriods.map( + ({ + startPeriod, + startPeriodYear, + endPeriod, + endPeriodYear, + tillfallesperioderNummer, + }: { + startPeriod: string + startPeriodYear: number + endPeriod: string + endPeriodYear: number + tillfallesperioderNummer: number + }) => periodsStr(startPeriod, startPeriodYear, endPeriod, endPeriodYear, tillfallesperioderNummer, language) + ) + + const areAllPeriodTextsEmpty = allPeriodTexts.every((value: string) => value === '') + + let educationalLevelText = '' + allEducationalLevels.forEach(({ educationalLevel }: { educationalLevel: string }, index: number) => { + educationalLevelText += `${educationalLevel}${index + 1 != allEducationalLevels.length ? '\n' : ''}` + }) + + let courseLanguageText = '' + allLanguages.forEach(({ courseLanguage }: { courseLanguage: string }, index: number) => { + courseLanguageText += `${courseLanguage}${index + 1 != allLanguages.length ? '\n' : ''}` + }) + + let coursePaceText = '' + allStudyPaces.forEach(({ coursePace }: { coursePace: number }, index: number) => { + coursePaceText += `${coursePace}%${index + 1 != allStudyPaces.length ? '\n' : ''}` + }) + + let courseCampusText = '' + allCampuses.forEach(({ courseCampus }: { courseCampus: string }, index: number) => { + courseCampusText += `${courseCampus}${index + 1 != allCampuses.length ? '\n' : ''}` + }) + + let periodText = areAllPeriodTextsEmpty ? courseHasNoRoundsInTableCell : '' + if (!areAllPeriodTextsEmpty) { + allPeriodTexts.forEach((text: string, index: number) => { + periodText += ` ${text} ${index + 1 != allPeriodTexts.length ? '\n' : ''}` + }) + } + + return [ + codeCell(courseCode, startTerm, language), + titleCell(courseCode, title, startTerm, language), + credits, + educationalLevelText, + courseLanguageText, + coursePaceText, + courseCampusText, + periodText, + ] + }) return parsedCourses } diff --git a/public/js/app/util/searchApi.ts b/public/js/app/util/searchApi.ts index ad0c5b55..3b42d5f3 100644 --- a/public/js/app/util/searchApi.ts +++ b/public/js/app/util/searchApi.ts @@ -1,17 +1,17 @@ // Importing fetch // Note: fetch is natively available in modern browsers and Node.js (with a fetch polyfill if needed). -import { KoppsCourseSearchParams, KoppsCourseSearchResult } from "./types/SearchApiTypes" +import { CourseSearchParams, CourseSearchResult } from "./types/SearchApiTypes" -export async function koppsCourseSearch( +export async function courseSearch( language: string, proxyUrl: string, - params: KoppsCourseSearchParams -): Promise { + params: CourseSearchParams +): Promise { try { // Constructing the URL with query parameters const baseUrl = new URL(proxyUrl, window.location.origin).href - const url = new URL(`${baseUrl}/intern-api/sok/${language}`) + const url = new URL(`${baseUrl}/intern-api/sokBeta/${language}`) Object.keys(params).forEach(key => { if (Array.isArray(params[key])) { params[key].forEach((item: any) => url.searchParams.append(`${key}[]`, item)) @@ -24,14 +24,14 @@ export async function koppsCourseSearch( const response = await fetch(url.toString()) if (!response.ok) { - return 'ERROR-koppsCourseSearch-' + response.status + return 'ERROR-courseSearch-' + response.status } - const data: KoppsCourseSearchResult | string = await response.json() + const data: CourseSearchResult | string = await response.json() return data } catch (error: any) { if (error instanceof Error) { - throw new Error('Unexpected error from koppsCourseSearch-' + error.message) + throw new Error('Unexpected error from courseSearch-' + error.message) } throw error } diff --git a/public/js/app/util/types/SearchApiTypes.ts b/public/js/app/util/types/SearchApiTypes.ts index fa521048..149934e8 100644 --- a/public/js/app/util/types/SearchApiTypes.ts +++ b/public/js/app/util/types/SearchApiTypes.ts @@ -1,23 +1,22 @@ import { ERROR_ASYNC, STATUS } from '../../hooks/searchUseAsync' import { ErrorAsync, Status } from '../../hooks/types/UseCourseSearchTypes' -export interface KoppsCourseSearchParams { +export interface CourseSearchParams { [key: string]: any // Allowing any key-value pair as params } export interface SearchHits { - searchHitInterval?: { [key: string]: any } - course: { [key: string]: any } + [key: string]: any } -export interface KoppsCourseSearchResult { +export interface CourseSearchResult { searchHits?: SearchHits[] errorCode?: ErrorAsync errorMessage?: string } -export interface KoppsCourseSearchResultState { - data: KoppsCourseSearchResult +export interface CourseSearchResultState { + data: CourseSearchResult status: Status | null error: ErrorAsync | null } diff --git a/public/js/app/util/types/SearchHelperTypes.ts b/public/js/app/util/types/SearchHelperTypes.ts index 1701c27b..3734abb2 100644 --- a/public/js/app/util/types/SearchHelperTypes.ts +++ b/public/js/app/util/types/SearchHelperTypes.ts @@ -25,9 +25,10 @@ export type TitleCell = ( export type PeriodsStrType = ( startPeriod: number | string, - startTerm: string | undefined, + startPeriodYear: number, endPeriod: number | string | undefined, - endTerm: string | undefined, + endPeriodYear: number, + tillfallesperioderNummer: number, language: string ) => string @@ -42,10 +43,6 @@ type DataItem = sortKey: string } -export type SortAndParseByCourseCodeForTableViewType = ( - courses: Course[], - sliceUntilNum: number, - language: string -) => DataItem[][] +export type SortAndParseByCourseCodeForTableViewType = (courses: Course[], language: string) => DataItem[][] -export type FlatCoursesArrType = (searchHits: SearchHits[]) => { courses: Course[]; hasSearchHitInterval: boolean } \ No newline at end of file +export type FlatCoursesArrType = (searchHits: SearchHits[]) => { courses: Course[]; hasSearchHitInterval: boolean } diff --git a/server/controllers/newSearchPageCtrl.js b/server/controllers/newSearchPageCtrl.js index 5a23c9c7..b6c52430 100644 --- a/server/controllers/newSearchPageCtrl.js +++ b/server/controllers/newSearchPageCtrl.js @@ -7,12 +7,14 @@ const i18n = require('../../i18n') // eslint-disable-next-line no-unused-vars const koppsApi = require('../kopps/koppsApi') +const { searchCourses } = require('../ladok/ladokApi') const { browser: browserConfig, server: serverConfig } = require('../configuration') const { createBreadcrumbs } = require('../utils/breadcrumbUtil') const { getServerSideFunctions } = require('../utils/serverSideRendering') const { compareSchools, filterOutDeprecatedSchools } = require('../../domain/schools') const { stringifyKoppsSearchParams } = require('../../domain/searchParams') +const term = require('../../domain/term') async function renderSearchPage( req, @@ -87,21 +89,58 @@ async function searchThirdCycleCourses(req, res, next) { }) } -async function performCourseSearch(req, res, next) { +async function performCourseSearchBeta(req, res, next) { const { lang } = req.params const { query } = req - // Example: `text_pattern=${pattern}` - const searchParamsStr = stringifyKoppsSearchParams(query) + + // const convertedPeriods = query.period?.reduce((acc, period) => { + // const splitedPeriod = period.split(':') + // const codes = + // splitedPeriod[1] === 'summer' ? [`VT${splitedPeriod[0]}`, `HT${splitedPeriod[0]}`] : [splitedPeriod[0]] + // const value = splitedPeriod[1] === 'summer' ? ['0', '5'] : splitedPeriod[1] + + // codes.forEach(code => { + // const existingEntry = acc.find(entry => entry.code === code) + + // if (existingEntry) { + // existingEntry.periods = Array.isArray(existingEntry.periods) + // ? [...existingEntry.periods, ...value] + // : [existingEntry.periods, ...value] + // } else { + // acc.push({ + // code: code, + // periods: value, + // }) + // } + // }) + // return acc + // }, []) // todo - we can use it again when we had the data for periods from ladok + + const convertedEduLevels = query.eduLevel?.map(level => { + if (level === '0') return 'FUPKURS' + if (level === '1') return '2007GKURS' + if (level === '2') return '2007AKURS' + if (level === '3') return '2007FKURS' + }) // todo - this can be moved to search params after we decided to use the beta search as the main search + + const searchParams = { + kodEllerBenamning: query.pattern ? query.pattern : undefined, + organisation: query.department ? query.department : undefined, + sprak: query.showOptions?.includes('onlyEnglish') ? 'ENG' : undefined, + avvecklad: query.showOptions?.includes('showCancelled') ? 'true' : undefined, + startPeriod: query.semesters ?? undefined, + utbildningsniva: convertedEduLevels ?? undefined, + } try { - log.debug(` trying to perform a search of courses with ${searchParamsStr} transformed from parameters: `, { query }) + log.debug(` trying to perform a search of courses with ${searchParams} transformed from parameters: `, { query }) - const apiResponse = await koppsApi.getSearchResults(searchParamsStr, lang) - log.debug(` performCourseSearch with ${searchParamsStr} response: `, apiResponse) + const apiResponse = await searchCourses(searchParams, lang) + log.debug(` performCourseSearch with ${searchParams} response: `, apiResponse) return res.json(apiResponse) } catch (error) { - log.error(` Exception from performCourseSearch with ${searchParamsStr}`, { error }) + log.error(` Exception from performCourseSearch with ${searchParams}`, { error }) next(error) } } @@ -132,5 +171,5 @@ async function _fillApplicationStoreWithAllSchools({ applicationStore, lang }) { module.exports = { searchAllCourses, searchThirdCycleCourses, - performCourseSearch, + performCourseSearchBeta, } diff --git a/server/ladok/ladokApi.js b/server/ladok/ladokApi.js new file mode 100644 index 00000000..81d82aab --- /dev/null +++ b/server/ladok/ladokApi.js @@ -0,0 +1,14 @@ +'use strict' + +const { createApiClient } = require('om-kursen-ladok-client') +const serverConfig = require('../configuration').server + +async function searchCourses(pattern, lang) { + const client = createApiClient(serverConfig.ladokMellanlagerApi) + const courses = await client.searchCourses(pattern, lang) + return courses +} + +module.exports = { + searchCourses, +} diff --git a/server/server.js b/server/server.js index 6cfb5add..4d82aa55 100644 --- a/server/server.js +++ b/server/server.js @@ -267,7 +267,12 @@ appRoute.get( Search.searchThirdCycleCourses ) -appRoute.get('api.searchCourses', proxyPrefixPath.courseSearchInternApi + '/:lang', NewSearchPage.performCourseSearch) +appRoute.get('api.searchCourses', proxyPrefixPath.courseSearchInternApi + '/:lang', Search.performCourseSearch) +appRoute.get( + 'api.searchCoursesBeta', + proxyPrefixPath.courseSearchInternApiBeta + '/:lang', + NewSearchPage.performCourseSearchBeta +) appRoute.post('api.programmeSyllabusPDF', proxyPrefixPath.programmeSyllabusPDF, PDFExport.performPDFRenderFunction) appRoute.get('redirect.departmentsListThirdCycleStudy', redirectProxyPath.thirdCycleRoot, (req, res) => { From 4222ce3a24d99868b86b56ef5d695d52c1b189ac Mon Sep 17 00:00:00 2001 From: AmirHossein Haerian <44199492+amirhossein-haerian@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:54:20 +0200 Subject: [PATCH 2/5] feat(KP-373): replace the old search with the newer version (#299) * feat(KP-373): replace the old search with the newer version * fix(KP-373): remove old search from more places --- config/commonSettings.js | 9 +- domain/searchParams.js | 16 +- i18n/messages.en.js | 19 - i18n/messages.se.js | 19 - public/js/app/app.jsx | 69 +- public/js/app/components/SearchAlert/types.ts | 2 +- .../js/app/components/SearchDepartments.jsx | 88 -- .../index.tsx | 4 +- .../types.ts | 0 .../js/app/components/SearchFilters/index.tsx | 19 +- public/js/app/components/SearchFormFields.jsx | 117 -- public/js/app/components/SearchInputField.jsx | 70 - public/js/app/components/SearchOptions.jsx | 97 -- .../index.tsx | 0 .../types.ts | 2 +- .../js/app/components/SearchResultDisplay.jsx | 174 -- .../components/SearchResultDisplay.test.js | 258 --- .../ListView.test.tsx | 0 .../ListView.tsx | 2 +- .../SearchResultComponent.tsx | 0 .../SearchResultDisplay.test.tsx | 14 +- .../SearchResultHeader.tsx | 0 .../TableView.test.tsx | 36 +- .../TableView.tsx | 2 +- .../index.tsx | 6 +- .../style.scss | 0 .../types.ts | 0 public/js/app/components/SearchTableView.jsx | 164 -- .../js/app/components/SearchTableView.test.js | 271 ---- .../ThirdCycleStudySearchFormFields.jsx | 97 -- .../SearchResultDisplay.test.js.snap | 495 ------ .../SearchTableView.test.js.snap | 863 ---------- public/js/app/components/index.js | 23 +- .../js/app/components/mocks/mockSearchHits.ts | 438 +---- public/js/app/config/menuData.js | 14 +- public/js/app/config/thirdCycleMenuData.js | 13 +- public/js/app/hooks/useCourseSearchParams.ts | 4 +- public/js/app/pages/CourseSearch.jsx | 134 -- public/js/app/pages/CourseSearch.test.js | 196 --- .../js/app/pages/CourseSearchLayout.test.js | 408 ----- .../app/pages/CourseSearchThirdCycleStudy.jsx | 138 -- .../js/app/pages/SearchLandingPage.test.tsx | 27 +- ...hLandingPage.tsx => SearchLandingPage.tsx} | 14 +- public/js/app/pages/SearchPage.test.tsx | 12 +- .../{NewSearchPage.tsx => SearchPage.tsx} | 18 +- .../CourseSearchLayout.test.js.snap | 1405 ----------------- .../pages/mocks/Appendix1ApplicationStore.js | 12 +- .../js/app/stores/createApplicationStore.js | 11 +- ...wSearchPageStore.ts => searchPageStore.ts} | 3 +- .../app/stores/types/searchPageStoreTypes.ts | 1 - public/js/app/util/internApi.js | 24 - public/js/app/util/searchApi.ts | 2 +- public/js/app/util/searchHelper.js | 31 - .../{newSearchHelper.ts => searchHelper.ts} | 2 +- server/controllers/index.js | 3 +- server/controllers/searchCtrl.js | 167 -- server/controllers/searchCtrl.test.js | 175 -- ...newSearchPageCtrl.js => searchPageCtrl.js} | 14 +- server/controllers/searchPageCtrl.test.js | 192 +++ server/kopps/koppsApi.js | 14 - server/mocks/mockLadokApi.js | 297 ++++ server/server.js | 27 +- 62 files changed, 634 insertions(+), 6098 deletions(-) delete mode 100644 public/js/app/components/SearchDepartments.jsx rename public/js/app/components/{NewSearchDepartments => SearchDepartments}/index.tsx (95%) rename public/js/app/components/{NewSearchDepartments => SearchDepartments}/types.ts (100%) delete mode 100644 public/js/app/components/SearchFormFields.jsx delete mode 100644 public/js/app/components/SearchInputField.jsx delete mode 100644 public/js/app/components/SearchOptions.jsx rename public/js/app/components/{NewSearchOptions => SearchOptions}/index.tsx (100%) rename public/js/app/components/{NewSearchOptions => SearchOptions}/types.ts (86%) delete mode 100644 public/js/app/components/SearchResultDisplay.jsx delete mode 100644 public/js/app/components/SearchResultDisplay.test.js rename public/js/app/components/{NewSearchResultDisplay => SearchResultDisplay}/ListView.test.tsx (100%) rename public/js/app/components/{NewSearchResultDisplay => SearchResultDisplay}/ListView.tsx (99%) rename public/js/app/components/{NewSearchResultDisplay => SearchResultDisplay}/SearchResultComponent.tsx (100%) rename public/js/app/components/{NewSearchResultDisplay => SearchResultDisplay}/SearchResultDisplay.test.tsx (82%) rename public/js/app/components/{NewSearchResultDisplay => SearchResultDisplay}/SearchResultHeader.tsx (100%) rename public/js/app/components/{NewSearchResultDisplay => SearchResultDisplay}/TableView.test.tsx (86%) rename public/js/app/components/{NewSearchResultDisplay => SearchResultDisplay}/TableView.tsx (98%) rename public/js/app/components/{NewSearchResultDisplay => SearchResultDisplay}/index.tsx (85%) rename public/js/app/components/{NewSearchResultDisplay => SearchResultDisplay}/style.scss (100%) rename public/js/app/components/{NewSearchResultDisplay => SearchResultDisplay}/types.ts (100%) delete mode 100644 public/js/app/components/SearchTableView.jsx delete mode 100644 public/js/app/components/SearchTableView.test.js delete mode 100644 public/js/app/components/ThirdCycleStudySearchFormFields.jsx delete mode 100644 public/js/app/components/__snapshots__/SearchResultDisplay.test.js.snap delete mode 100644 public/js/app/components/__snapshots__/SearchTableView.test.js.snap delete mode 100644 public/js/app/pages/CourseSearch.jsx delete mode 100644 public/js/app/pages/CourseSearch.test.js delete mode 100644 public/js/app/pages/CourseSearchLayout.test.js delete mode 100644 public/js/app/pages/CourseSearchThirdCycleStudy.jsx rename public/js/app/pages/{NewSearchLandingPage.tsx => SearchLandingPage.tsx} (91%) rename public/js/app/pages/{NewSearchPage.tsx => SearchPage.tsx} (81%) delete mode 100644 public/js/app/pages/__snapshots__/CourseSearchLayout.test.js.snap rename public/js/app/stores/{newSearchPageStore.ts => searchPageStore.ts} (91%) delete mode 100644 public/js/app/util/internApi.js delete mode 100644 public/js/app/util/searchHelper.js rename public/js/app/util/{newSearchHelper.ts => searchHelper.ts} (98%) delete mode 100644 server/controllers/searchCtrl.js delete mode 100644 server/controllers/searchCtrl.test.js rename server/controllers/{newSearchPageCtrl.js => searchPageCtrl.js} (94%) create mode 100644 server/controllers/searchPageCtrl.test.js create mode 100644 server/mocks/mockLadokApi.js diff --git a/config/commonSettings.js b/config/commonSettings.js index 839cf237..f154c169 100644 --- a/config/commonSettings.js +++ b/config/commonSettings.js @@ -15,19 +15,16 @@ module.exports = { // The proxy prefix path if the application is proxied. E.g /places proxyPrefixPath: { uri: studentRoot, - courseSearch: `${studentRoot}/sokkurs`, - newSearchPage: `${studentRoot}/sokkurs-beta`, - searchResult: `${studentRoot}/sokkurs-beta/resultat`, + searchPage: `${studentRoot}/sokkurs`, + searchResult: `${studentRoot}/sokkurs/resultat`, courseSearchInternApi: `${studentRoot}/intern-api/sok`, - courseSearchInternApiBeta: `${studentRoot}/intern-api/sokBeta`, department: `${studentRoot}/org`, programme: `${studentRoot}/program`, programmesList: `${studentRoot}/kurser-inom-program`, schoolsList: `${studentRoot}/program`, studyHandbook: '/student/program/shb', thirdCycleCourseSearch: `${thirdCycleRoot}/sok`, - thirdCycleCourseSearchNew: `${thirdCycleRoot}/sok-beta`, - thirdCycleCourseSearchResultNew: `${thirdCycleRoot}/sok-beta/resultat`, + thirdCycleCourseSearchResult: `${thirdCycleRoot}/sok/resultat`, thirdCycleSchoolsAndDepartments: `${thirdCycleRoot}/avdelning`, thirdCycleCoursesPerDepartment: `${thirdCycleRoot}/org`, literatureList: `${studentRoot}/lit`, diff --git a/domain/searchParams.js b/domain/searchParams.js index 39b1a824..2d87b28e 100644 --- a/domain/searchParams.js +++ b/domain/searchParams.js @@ -102,7 +102,7 @@ function _combineTermsByYear(arrWithYearsAndPeriod) { return groupedTerms } -function _periodConfigForOneYear({ year, terms }, langIndex, isBetaSearch) { +function _periodConfigForOneYear({ year, terms }, langIndex) { const hasOnlyOneTerm = !!terms.length === 1 const { summer: summerLabel } = i18n.messages[langIndex].bigSearch @@ -130,7 +130,7 @@ function _periodConfigForOneYear({ year, terms }, langIndex, isBetaSearch) { value, }) } - const value = isBetaSearch ? `${term == 2 ? 'HT' : 'VT'}${year}:${periodNum}` : `${year}${term}:${periodNum}` + const value = `${term == 2 ? 'HT' : 'VT'}${year}:${periodNum}` const label = `${formatLongTerm(`${year}${term}`, language)} period ${periodNum}` return resultPeriodsConfig.push({ label, id: value, value }) }) @@ -139,15 +139,15 @@ function _periodConfigForOneYear({ year, terms }, langIndex, isBetaSearch) { return resultPeriodsConfig } -function _periodConfigByYearType(yearType, langIndex, isBetaSearch = false) { +function _periodConfigByYearType(yearType, langIndex) { const relevantTerms = getRelevantTerms(2) const yearsAndPeriod = _separateYearAndPeriod(relevantTerms) const { current, next } = _combineTermsByYear(yearsAndPeriod) switch (yearType) { case 'currentYear': - return _periodConfigForOneYear(current, langIndex, isBetaSearch) + return _periodConfigForOneYear(current, langIndex) case 'nextYear': - return _periodConfigForOneYear(next, langIndex, isBetaSearch) + return _periodConfigForOneYear(next, langIndex) default: throw new Error(`Unknown yearType: ${yearType}. Allowed values: currentYear and nextYear`) } @@ -176,12 +176,8 @@ function getParamConfig(paramName, langIndex) { case 'eduLevel': return eduLevelConfig(langIndex) case 'currentYear': - return _periodConfigByYearType('currentYear', langIndex) - case 'nextYear': - return _periodConfigByYearType('nextYear', langIndex) - case 'currentYearBeta': return _periodConfigByYearType('currentYear', langIndex, true) - case 'nextYearBeta': + case 'nextYear': return _periodConfigByYearType('nextYear', langIndex, true) case 'semesters': return _semestersConfig(langIndex) diff --git a/i18n/messages.en.js b/i18n/messages.en.js index de60526b..e3aeefae 100644 --- a/i18n/messages.en.js +++ b/i18n/messages.en.js @@ -95,10 +95,8 @@ const messages = { main_menu_page_example: 'Example', main_menu_shb: 'Studies before 07/08', main_menu_search_all: 'Search courses', - main_menu_search_all_new: 'Search courses (beta)', main_menu_third_cycle_studies: 'Doctoral studies (PhD)', main_menu_third_cycle_courses_search: 'Search third-cycle courses', - main_menu_third_cycle_courses_search_new: 'Search third-cycle courses (beta)', main_page_header_example: 'Example', main_page_article_lead_example: @@ -283,10 +281,6 @@ const messages = { 'You may select to show courses that are no longer offered or dormant at KTH (terminated courses). By default, these are not shown.', search_help_10: 'If you have questions or feedback on the course search, please contact kopps@kth.se.', - beta_version_title: 'Want to try our new search function?', - beta_version_description: - 'We have designed and developed a new search function for the Course and Program directory that you can test here!', - beta_version_link: 'Search courses (beta)', }, thirdCycleSearchInstructions: { search_help_collapse_header: 'Instructions for searching', @@ -298,10 +292,6 @@ const messages = { search_research_help_5: 'Courses that are no longer offered or dormant at KTH are not shown.', search_research_help_6: 'You can filter on courses that deal with environment, environmental technology, or sustainable development.', - beta_version_title: 'Want to try our new search function?', - beta_version_description: - 'We have designed and developed a new search function for the Course and Program directory that you can test here! You can and are welcome to leave feedback on what you think of the new search function!', - beta_version_link: 'Search third-cycle courses (beta)', }, generalSearch: { collapseHeaderOtherSearchOptions: 'Filter your search choices', @@ -343,19 +333,10 @@ const messages = { errorUnknown: { text: 'An unknown error occurred - failed to retrieve course data' }, errorKodEllerBenamning: { text: 'Search input must be equal or more than 3 characters.' }, errorEmpty: { - header: 'Your search returned no results', - help: 'For help, see the link below: Instructions for searching', - }, - errorEmptyBeta: { header: 'Your search returned no results', // we don't have this link anymore so we should decide what we are going to show as a help text }, errorOverflow: { - header: 'There were too many results', - text: 'There are too many courses that match your search critera. Please specify more characters/digits in the course name or course code (example of course code: SF1624)', - help: 'For help, see the link below: Instructions for searching', - }, - errorOverflowBeta: { header: 'There were too many results', text: 'There are too many courses that match your search critera. Please specify more characters/digits in the course name or course code (example of course code: SF1624)', // we don't have this link anymore so we should decide what we are going to show as a help text diff --git a/i18n/messages.se.js b/i18n/messages.se.js index f2e383ff..379cb54f 100644 --- a/i18n/messages.se.js +++ b/i18n/messages.se.js @@ -94,10 +94,8 @@ const messages = { main_menu_page_example: 'Exempel', main_menu_shb: 'Studier före 07/08', main_menu_search_all: 'Sök kurser', - main_menu_search_all_new: 'Sök kurser (beta)', main_menu_third_cycle_studies: 'Forskarutbildning', main_menu_third_cycle_courses_search: 'Sök forskarkurs', - main_menu_third_cycle_courses_search_new: 'Sök forskarkurs (beta)', main_page_header_example: 'Exempel', main_page_article_lead_example: '”Stora miljoner dag, har.”', @@ -275,10 +273,6 @@ const messages = { 'Du kan välja att även visa kurser som ej längre ges på KTH. Standardinställning är att dessa inte visas.', search_help_10: 'Om du har synpunkter eller frågor gällande kurssökningen, kontakta kopps@kth.se.', - beta_version_title: 'Vill du prova vår nya sökfunktion?', - beta_version_description: - 'Vi har designat och utvecklat en ny sökfunktion för Kurs- och programkatalogen som du kan testa här!', - beta_version_link: 'Sök kurser (beta)', }, thirdCycleSearchInstructions: { search_help_collapse_header: 'Få hjälp med sökningen', @@ -289,10 +283,6 @@ const messages = { 'Sökningen visar max 250 träffar. Får du för många träffar, försök att förfina sökvillkoren.', search_research_help_5: 'Forskarkurser som ej längre ges på KTH visas inte.', search_research_help_6: 'Du kan filtrera på kurser som behandlar miljö, miljöteknik eller hållbar utveckling.', - beta_version_title: 'Vill du prova vår nya sökfunktion?', - beta_version_description: - 'Vi har designat och utvecklat en ny sökfunktion för Kurs- och programkatalogen som du kan testa här! Du kan och får gärna lämna feedback på vad du tycker om den nya sökfunktionen!', - beta_version_link: 'Sök forskarkurs (beta)', }, generalSearch: { collapseHeaderOtherSearchOptions: 'Filtrera dina sökval', @@ -334,19 +324,10 @@ const messages = { errorUnknown: { text: 'Ett okänt fel inträffade - misslyckad hämtning av kursdata' }, errorKodEllerBenamning: { text: 'Sökinmatningen måste vara lika med eller större än 3 tecken.' }, errorEmpty: { - header: 'Din sökning gav inga träffar.', - help: 'Mer hjälp hittar du i länken nedan: Få hjälp med sökningen', - }, - errorEmptyBeta: { header: 'Din sökning gav inga träffar.', // we don't have this link anymore so we should decide what we are going to show as a help text }, errorOverflow: { - header: 'Sökningen gav för många träffar', - text: 'Det finns för många kurser som matchar det du sökt på. Ange fler bokstäver/siffror i kursnamn eller kurskod (exempel på kurskod: SF1624).', - help: 'Mer hjälp hittar du i länken nedan: Få hjälp med sökningen', - }, - errorOverflowBeta: { header: 'Sökningen gav för många träffar', text: 'Det finns för många kurser som matchar det du sökt på. Ange fler bokstäver/siffror i kursnamn eller kurskod (exempel på kurskod: SF1624).', // we don't have this link anymore so we should decide what we are going to show as a help text diff --git a/public/js/app/app.jsx b/public/js/app/app.jsx index 12a55d7c..56276946 100644 --- a/public/js/app/app.jsx +++ b/public/js/app/app.jsx @@ -12,9 +12,7 @@ import '../../css/node-web.scss' import Appendix1 from './pages/Appendix1' import Appendix2 from './pages/Appendix2' -import NewSearchLandingPage from './pages/NewSearchLandingPage' -import CourseSearch from './pages/CourseSearch' -import CourseSearchThirdCycleStudy from './pages/CourseSearchThirdCycleStudy' +import SearchLandingPage from './pages/SearchLandingPage' import Curriculum from './pages/Curriculum' import DepartmentCourses from './pages/DepartmentCourses' import DepartmentsList from './pages/DepartmentsList' @@ -31,7 +29,7 @@ import SearchPageWrapper from './components/SearchPageWrapper' import StudyHandbook from './pages/StudyHandbook' import ThirdCycleDepartmentsList from './pages/ThirdCycleDepartmentsList' import ProgramSyllabusExport from './pages/ProgramSyllabusExport' -import NewSearchPage from './pages/NewSearchPage' +import SearchPage from './pages/SearchPage' import getCurriculumMenuData from './config/curriculumMenuData' import getDepartmentMenuData from './config/departmentMenuData' @@ -81,7 +79,7 @@ function _renderOnClientSide() { function appFactory(serverSideApplicationStore = null) { const sharedInitApplicationStoreCallback = { - newSearchPage: () => _initStore({ storeId: 'newSearchPage' }), + SearchPage: () => _initStore({ storeId: 'SearchPage' }), } return ( @@ -112,68 +110,55 @@ function appFactory(serverSideApplicationStore = null) { } /> _initStore({ caller: 'CourseSearch' })} + initApplicationStoreCallback={sharedInitApplicationStoreCallback['SearchPage']} createMenuData={store => ({ selectedId: 'searchAllCourses', ...getMenuData(store) })} /> } /> ({ selectedId: 'searchAllCourses-new', ...getMenuData(store) })} - /> - } - /> - } /> ({ - selectedId: 'searchThirdCycleCoursesNew', + selectedId: 'searchThirdCycleCourses', ...getThirdCycleMenuData(store), })} /> } /> } /> @@ -251,22 +236,6 @@ function appFactory(serverSideApplicationStore = null) { /> } /> - _initStore({ storeId: 'searchCourses' })} - createMenuData={store => ({ - selectedId: 'searchThirdCycleCourses', - ...getThirdCycleMenuData(store), - })} - /> - } - /> { - let isMounted = true - if (isMounted) onChange({ department }) - return () => (isMounted = false) - }, [department]) - - function handleChange(e) { - setDepartment(e.target.value) - } - - return ( -
-
- {departmentLabel} - -
-
- ) -} -SearchDepartments.propTypes = { - onChange: PropTypes.func.isRequired, -} - -export default SearchDepartments diff --git a/public/js/app/components/NewSearchDepartments/index.tsx b/public/js/app/components/SearchDepartments/index.tsx similarity index 95% rename from public/js/app/components/NewSearchDepartments/index.tsx rename to public/js/app/components/SearchDepartments/index.tsx index 3d6097d3..5a9e4b12 100644 --- a/public/js/app/components/NewSearchDepartments/index.tsx +++ b/public/js/app/components/SearchDepartments/index.tsx @@ -6,7 +6,7 @@ import { localeCompareDepartments } from '../../../../../domain/departments' import { SearchDepartmentsProps, SchoolsWithDepartments } from './types' -const NewSearchDepartments: React.FC = ({ onChange, disabled, departmentCode }) => { +const SearchDepartments: React.FC = ({ onChange, disabled, departmentCode }) => { const store = useStore() const { currentSchoolsWithDepartments, deprecatedSchoolsWithDepartments, language, languageIndex } = store @@ -74,4 +74,4 @@ const NewSearchDepartments: React.FC = ({ onChange, disa ) } -export default NewSearchDepartments +export default SearchDepartments diff --git a/public/js/app/components/NewSearchDepartments/types.ts b/public/js/app/components/SearchDepartments/types.ts similarity index 100% rename from public/js/app/components/NewSearchDepartments/types.ts rename to public/js/app/components/SearchDepartments/types.ts diff --git a/public/js/app/components/SearchFilters/index.tsx b/public/js/app/components/SearchFilters/index.tsx index d15a2dd1..22f4abf1 100644 --- a/public/js/app/components/SearchFilters/index.tsx +++ b/public/js/app/components/SearchFilters/index.tsx @@ -4,8 +4,8 @@ import { CollapseDetails } from '@kth/kth-reactstrap/dist/components/utbildnings import { useStore } from '../../mobx' import i18n from '../../../../../i18n' -import NewSearchDepartments from '../NewSearchDepartments' -import NewSearchOptions from '../NewSearchOptions' +import SearchDepartments from '../SearchDepartments' +import SearchOptions from '../SearchOptions' import { FilterParams, SearchFilterStore, SearchFiltersProps, FILTER_MODES } from './types' import { DepartmentCodeOrPrefix, @@ -47,7 +47,10 @@ const SearchFilters: React.FC = ({ } const hasActiveFilters = Object.entries(courseSearchParams).some(([key, value]) => { - if (key !== 'pattern') { + if ( + (searchMode !== SEARCH_MODES.thirdCycleCourses && key !== 'pattern') || + (searchMode === SEARCH_MODES.thirdCycleCourses && key !== 'pattern' && key !== 'eduLevel') + ) { return Array.isArray(value) ? value.length > 0 : !!value } return false // Ignore the 'pattern' key @@ -58,7 +61,7 @@ const SearchFilters: React.FC = ({ {filterMode.includes('semesters') && (
- = ({
{filterMode.includes('eduLevel') && (
- = ({ )} {filterMode.includes('eduLevel') && (
- = ({
)} {filterMode.includes('onlyMHU') && ( - = ({ {filterMode.includes('department') && (
- ({ ...state, ...action }) - -function SearchFormFields({ caption, openOptions, onSubmit }) { - const { languageIndex, textPattern: initialPattern = '' } = useStore() - - const { generalSearch } = i18n.messages[languageIndex] - const { searchLabel, searchStartPeriodPrefix, collapseHeaderOtherSearchOptions } = generalSearch - const [state, setState] = useReducer(paramsReducer, { pattern: initialPattern }) - const { pattern } = state - - const currentYearDate = new Date().getFullYear() - const currentYearLabel = `${searchStartPeriodPrefix} ${currentYearDate}` - const nextYearLabel = `${searchStartPeriodPrefix} ${Number(currentYearDate) + 1}` - - function handlePatternChange(e) { - const { value } = e.target - const cleanTextPattern = value ? value.replace(/['"<>$]+/g, '') : '' - setState({ pattern: cleanTextPattern }) - } - - function handleSubmit(e) { - e.preventDefault() - - setState({ pattern: pattern.trim() }) - - onSubmit(state) - } - - function handleParamChange(params) { - setState(params) - } - - return ( -
-
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ) -} - -SearchFormFields.propTypes = { - caption: PropTypes.string.isRequired, - onSubmit: PropTypes.func.isRequired, - openOptions: PropTypes.bool.isRequired, -} - -export default SearchFormFields diff --git a/public/js/app/components/SearchInputField.jsx b/public/js/app/components/SearchInputField.jsx deleted file mode 100644 index 8f9c2238..00000000 --- a/public/js/app/components/SearchInputField.jsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { useEffect, useState } from 'react' -import PropTypes from 'prop-types' - -import { useStore } from '../mobx' -import i18n from '../../../../i18n' - -function SearchInputField({ caption = 'N/A', pattern: externalPattern, onSubmit }) { - const { languageIndex } = useStore() - const [pattern, setPattern] = useState(externalPattern || '') - - const { generalSearch } = i18n.messages[languageIndex] - const { searchLabel } = generalSearch - - useEffect(() => { - let isMounted = true - - if (isMounted && typeof externalPattern === 'string') setPattern(externalPattern) - return () => (isMounted = false) - }, [externalPattern]) - - function handleChange(e) { - setPattern(e.target.value) - } - - function handleSubmit(e) { - e.preventDefault() - onSubmit(pattern) - } - - return ( -
-
- - -
- -
- ) -} - -SearchInputField.propTypes = { - caption: PropTypes.string.isRequired, - onSubmit: PropTypes.func.isRequired, - pattern: PropTypes.string, -} - -SearchInputField.defaultProps = { - pattern: '', -} - -export default SearchInputField diff --git a/public/js/app/components/SearchOptions.jsx b/public/js/app/components/SearchOptions.jsx deleted file mode 100644 index 64ba9470..00000000 --- a/public/js/app/components/SearchOptions.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useEffect } from 'react' -import PropTypes from 'prop-types' - -import { useStore } from '../mobx' -import i18n from '../../../../i18n' -import { getParamConfig } from '../../../../domain/searchParams' - -const optionsReducer = (state, action) => { - const { value, type } = action - const { options } = state - - switch (type) { - case 'ADD_ITEM': { - const lastIndex = options.length - - options[lastIndex] = value // because while it is in state it, turns array into ordered object - return { options } - } - case 'REMOVE_ITEM': { - const removeIndex = options.indexOf(value) - if (removeIndex >= 0) { - options.splice(removeIndex, 1) - } - return { options } - } - default: { - throw new Error( - `Cannot change the state in reducer. Unknown type of action: ${type}. Allowed options: ADD_ITEM, REMOVE_ITEM` - ) - } - } -} - -function SearchOptions({ overrideSearchHead = '', paramAliasName = '', paramName, onChange }) { - const store = useStore() - const { languageIndex } = store - const initialParamValue = store[paramName] - const [{ options }, setOptions] = React.useReducer(optionsReducer, { options: initialParamValue || [] }) - const { bigSearch } = i18n.messages[languageIndex] - const searchHeadLevel = overrideSearchHead || bigSearch[paramName] - - const values = React.useMemo( - () => getParamConfig(paramAliasName || paramName, languageIndex), - [paramAliasName, paramName, languageIndex] - ) - // [{ label, id, value}, ...] - - useEffect(() => { - let isMounted = true - - if (isMounted) onChange({ [paramName]: options }) - return () => (isMounted = false) - }, [options.length]) - - function handleChange(e) { - const { value, checked } = e.target - setOptions({ value, type: checked ? 'ADD_ITEM' : 'REMOVE_ITEM' }) - } - - return ( -
-
- {searchHeadLevel} - {values.map(({ label, id, value }) => ( -
- - -
- ))} -
-
- ) -} - -SearchOptions.propTypes = { - overrideSearchHead: PropTypes.string, - paramAliasName: PropTypes.oneOf(['currentYear', 'nextYear', 'onlyMHU', '']), - paramName: PropTypes.oneOf(['eduLevel', 'period', 'showOptions']).isRequired, - onChange: PropTypes.func.isRequired, -} - -SearchOptions.defaultProps = { - overrideSearchHead: '', - paramAliasName: '', -} - -export default SearchOptions diff --git a/public/js/app/components/NewSearchOptions/index.tsx b/public/js/app/components/SearchOptions/index.tsx similarity index 100% rename from public/js/app/components/NewSearchOptions/index.tsx rename to public/js/app/components/SearchOptions/index.tsx diff --git a/public/js/app/components/NewSearchOptions/types.ts b/public/js/app/components/SearchOptions/types.ts similarity index 86% rename from public/js/app/components/NewSearchOptions/types.ts rename to public/js/app/components/SearchOptions/types.ts index bb15437c..3e960e1a 100644 --- a/public/js/app/components/NewSearchOptions/types.ts +++ b/public/js/app/components/SearchOptions/types.ts @@ -14,7 +14,7 @@ export interface SearchOptionReturnValues { export interface SearchOptionsProps { overrideSearchHead?: string - paramAliasName?: 'currentYear' | 'nextYear' | 'currentYearBeta' | 'nextYearBeta' | 'onlyMHU' | '' + paramAliasName?: 'currentYear' | 'nextYear' | 'onlyMHU' | '' paramName: 'eduLevel' | 'semesters' | 'period' | 'showOptions' selectedValues: SearchOptionValues onChange: (params: SearchOptionReturnValues) => void diff --git a/public/js/app/components/SearchResultDisplay.jsx b/public/js/app/components/SearchResultDisplay.jsx deleted file mode 100644 index 00e1eaea..00000000 --- a/public/js/app/components/SearchResultDisplay.jsx +++ /dev/null @@ -1,174 +0,0 @@ -import React, { useEffect, useState } from 'react' -import { createRoot } from 'react-dom/client' -import { useNavigate } from 'react-router-dom' -import PropTypes from 'prop-types' -import koppsCourseSearch from '../util/internApi' -import { useStore } from '../mobx' - -import i18n from '../../../../i18n' -import { stringifyUrlParams } from '../../../../domain/searchParams' -import { CLIENT_EDU_LEVELS } from '../../../../domain/eduLevels' -import { CLIENT_SHOW_OPTIONS } from '../../../../domain/courseOptions' - -import { STATUS, ERROR_ASYNC, useAsync } from '../hooks/searchUseAsync' -import SearchTableView, { searchHitsPropsShape } from './SearchTableView' -import { SearchAlert } from './index' - -function _getThisHost(thisHostBaseUrl) { - return thisHostBaseUrl.slice(-1) === '/' ? thisHostBaseUrl.slice(0, -1) : thisHostBaseUrl -} - -function renderAlertToTop(errorType, languageIndex) { - const alertContainer = document.getElementById('alert-placeholder') - - if (alertContainer) { - const root = createRoot(alertContainer) - root.render() - } -} -function dismountTopAlert() { - const alertContainer = document.getElementById('alert-placeholder') - - if (alertContainer) { - const root = createRoot(alertContainer) - root.unmount() - } -} - -const errorItalicParagraph = (errorType, languageIndex) => { - const errorText = i18n.messages[languageIndex].generalSearch[errorType] - if (!errorText) - throw new Error( - `Missing translations for errorType: ${errorType}. Allowed types: ${Object.values(ERROR_ASYNC).join(', ')}` - ) - - return ( -

- {errorText} -

- ) -} - -function DisplayResult({ languageIndex, searchStatus, errorType, searchResults }) { - if (searchStatus === STATUS.resolved) { - return - } - - if (searchStatus === STATUS.idle) return null - if (searchStatus === STATUS.pending) { - const { searchLoading } = i18n.messages[languageIndex].generalSearch - return

{searchLoading}

- } - if (errorType) return errorItalicParagraph(errorType, languageIndex) - - return null -} - -DisplayResult.propTypes = { - languageIndex: PropTypes.oneOf([0, 1]), - searchStatus: PropTypes.oneOf([...Object.values(STATUS), null]), - errorType: PropTypes.oneOf([...Object.values(ERROR_ASYNC), '']), - searchResults: PropTypes.shape(searchHitsPropsShape), -} - -DisplayResult.defaultProps = { - languageIndex: 0, - errorType: '', - searchResults: {}, - searchStatus: null, -} - -function SearchResultDisplay({ searchParameters, onlyPattern = false }) { - const { browserConfig, language, languageIndex } = useStore() - const navigate = useNavigate() - const { pattern } = searchParameters - const searchStr = stringifyUrlParams(searchParameters) - const [loadStatus, setLoadStatus] = useState('firstLoad') - const { resultsHeading } = i18n.messages[languageIndex].generalSearch - - const asyncCallback = React.useCallback(() => { - if (onlyPattern && !pattern) return - if (loadStatus === 'firstLoad') { - setLoadStatus('afterLoad') - - if (!searchStr) return - } - - const proxyUrl = _getThisHost(browserConfig.proxyPrefixPath.uri) - // eslint-disable-next-line consistent-return - return koppsCourseSearch(language, proxyUrl, searchParameters) - }, [searchParameters]) - - const initialStatus = onlyPattern - ? { status: pattern ? STATUS.pending : STATUS.idle } - : { status: searchStr ? STATUS.pending : STATUS.idle } - - const state = useAsync(asyncCallback, initialStatus) - - const { data: searchResults, status: searchStatus, error: errorType } = state - - useEffect(() => { - let isMounted = true - if (isMounted) { - if (errorType && errorType !== null) { - renderAlertToTop(errorType, languageIndex) - } else dismountTopAlert() - } - return () => (isMounted = false) - }, [searchStatus]) - - useEffect(() => { - if (!navigate) return - if ((onlyPattern && pattern) || !onlyPattern) { - if (searchStatus === STATUS.pending) { - navigate({ search: searchStr }, { replace: true }) - } - } - }, [searchStatus]) - - return ( - <> - {searchStatus !== STATUS.idle &&

{resultsHeading}

} - - - ) -} - -SearchResultDisplay.propTypes = { - onlyPattern: PropTypes.bool, - searchParameters: PropTypes.shape({ - eduLevel: PropTypes.arrayOf(PropTypes.oneOf(CLIENT_EDU_LEVELS)), - pattern: PropTypes.string, - // eslint-disable-next-line consistent-return - // eslint-disable-next-line no-shadow - period: PropTypes.arrayOf((propValue, key, componentName, location, propFullName) => { - if (!/\b\d{5}\b:(\b\d)/.test(propValue[key]) && !/\b\d{4}\b:(summer)/.test(propValue[key])) { - return new Error( - `Prop ${propFullName} of a component ${componentName} has incorrect prop value ${propValue[key]}` - ) - } - }), - showOptions: PropTypes.arrayOf(PropTypes.oneOf(CLIENT_SHOW_OPTIONS)), - // eslint-disable-next-line consistent-return - // eslint-disable-next-line no-shadow - department: (propValue, key, componentName, location, propFullName) => { - if (!/[A-Za-zÅÄÖåäö]{1,4}/.test(propValue[key])) { - return new Error( - `Prop ${propFullName} of a component ${componentName} has incorrect prop value ${propValue[key]}` - ) - } - }, - }), -} - -SearchResultDisplay.defaultProps = { - onlyPattern: false, - searchParameters: {}, -} - -export default SearchResultDisplay diff --git a/public/js/app/components/SearchResultDisplay.test.js b/public/js/app/components/SearchResultDisplay.test.js deleted file mode 100644 index bb5c5ecd..00000000 --- a/public/js/app/components/SearchResultDisplay.test.js +++ /dev/null @@ -1,258 +0,0 @@ -import React from 'react' -import { render, screen, act, waitForElementToBeRemoved } from '@testing-library/react' -import '@testing-library/jest-dom' -import koppsCourseSearch from '../util/internApi' -import SearchResultDisplay from './SearchResultDisplay' -import { useStore } from '../mobx' -import { - TEST_API_ANSWER_ALGEBRA, - TEST_API_ANSWER_EMPTY_PARAMS, - TEST_API_ANSWER_OVERFLOW, - TEST_API_ANSWER_UNKNOWN_ERROR, - TEST_API_ANSWER_NO_HITS, - TEST_API_ANSWER_RESOLVED, -} from './mocks/mockKoppsCourseSearch' - -jest.setTimeout(1000) - -jest.mock('../mobx') -jest.mock('../util/internApi') -jest.mock('react-router-dom', () => ({ - useNavigate: jest.fn(), -})) - -describe('Component and its warning messages', () => { - test('double search: twice render empty parameters, first time without asking kopps, second time show a warning message. English. 1A', async () => { - useStore.mockReturnValue({ - browserConfig: { proxyPrefixPath: { uri: '/student/kurser' } }, - language: 'en', - languageIndex: 0, - }) - const { asFragment, rerender } = render() - - expect(koppsCourseSearch).not.toHaveBeenCalled() - - expect(asFragment()).toMatchSnapshot() - - koppsCourseSearch.mockReturnValue(Promise.resolve(TEST_API_ANSWER_EMPTY_PARAMS)) - - act(() => rerender()) - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', {}) - await waitForElementToBeRemoved(() => screen.getByText(/searching/i)) - - try { - expect(screen.getByText(/Search results/i)).toBeInTheDocument() - expect(screen.getByText(/No query restriction was specified/i)).toBeInTheDocument() - } catch (error) { - error.message = `${`Because it is a second try to search with empty params it must give a warning message\n\n ${error}`} ` - throw error - } - - expect(asFragment()).toMatchSnapshot() - }) - - test('double search: by a text pattern and rerender it again with the empty parameter to get a warning message noQueryProvided. English. 2A', async () => { - useStore.mockReturnValue({ - browserConfig: { proxyPrefixPath: { uri: '/student/kurser' } }, - language: 'en', - languageIndex: 0, - }) - - koppsCourseSearch.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - - const { asFragment, rerender } = render() - expect(screen.getByText(/searching/i)).toBeInTheDocument() - - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: 'Algebra' }) - - // rerender with empty params - koppsCourseSearch.mockReturnValue(Promise.resolve(TEST_API_ANSWER_EMPTY_PARAMS)) - - act(() => rerender()) - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', {}) - await waitForElementToBeRemoved(() => screen.getByText(/searching/i)) - - try { - expect(screen.getByText(/Search results/i)).toBeInTheDocument() - expect(screen.getByText(/No query restriction was specified/i)).toBeInTheDocument() - } catch (error) { - error.message = ` ${`Because it is a second try to search with empty params it must give a warning message\n\n ${error}`} ` - throw error - } - - expect(asFragment()).toMatchSnapshot() - }) - - test('double search: show an overflow warning message and then show a message about empty results "noHits". English. 3A', async () => { - useStore.mockReturnValue({ - browserConfig: { proxyPrefixPath: { uri: '/student/kurser' } }, - language: 'en', - languageIndex: 0, - }) - - koppsCourseSearch.mockReturnValue(Promise.resolve(TEST_API_ANSWER_OVERFLOW)) - - const { asFragment, rerender } = render() - expect(screen.getByText(/searching/i)).toBeInTheDocument() - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: 'A' }) - - await waitForElementToBeRemoved(() => screen.getByText(/searching/i)) - expect(screen.getByText(/Search results/i)).toBeInTheDocument() - expect(screen.getByText(/There were too many results./i)).toBeInTheDocument() - - // rerender - koppsCourseSearch.mockReturnValue(Promise.resolve(TEST_API_ANSWER_NO_HITS)) - - act(() => rerender()) - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: 'CHOKLADKAKA' }) - await waitForElementToBeRemoved(() => screen.getByText(/searching/i)) - - expect(screen.getByText(/Search results/i)).toBeInTheDocument() - expect(screen.getByText(/Your search returned no results./i)).toBeInTheDocument() - - expect(asFragment()).toMatchSnapshot() - }) - - test('double search: show empty results message "noHits" and then show a rejected message of unknown error. English. 4A', async () => { - useStore.mockReturnValue({ - browserConfig: { proxyPrefixPath: { uri: '/student/kurser' } }, - language: 'en', - languageIndex: 0, - }) - - koppsCourseSearch.mockReturnValue(Promise.resolve(TEST_API_ANSWER_NO_HITS)) - - const { asFragment, rerender } = render() - expect(screen.getByText(/searching/i)).toBeInTheDocument() - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: 'CHOKLADKAKA' }) - await waitForElementToBeRemoved(() => screen.getByText(/searching/i)) - - expect(screen.getByText(/Search results/i)).toBeInTheDocument() - expect(screen.getByText(/Your search returned no results./i)).toBeInTheDocument() - - expect(asFragment()).toMatchSnapshot() - - // rerender - koppsCourseSearch.mockReturnValue(Promise.reject(TEST_API_ANSWER_UNKNOWN_ERROR)) - - act(() => rerender()) - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: 'CHOKLADKAKA' }) - await waitForElementToBeRemoved(() => screen.getByText(/searching/i)) - - expect(screen.getByText(/Search results/i)).toBeInTheDocument() - expect(screen.getByText(/An unknown error occurred - failed to retrieve course data./i)).toBeInTheDocument() - - expect(asFragment()).toMatchSnapshot() - }) - - test('search:show a rejected message of unknown error. English. 5A', async () => { - useStore.mockReturnValue({ - browserConfig: { proxyPrefixPath: { uri: '/student/kurser' } }, - language: 'en', - languageIndex: 0, - }) - - // rerender - koppsCourseSearch.mockReturnValue(Promise.reject(TEST_API_ANSWER_UNKNOWN_ERROR)) - - const { asFragment } = render() - expect(screen.getByText(/searching/i)).toBeInTheDocument() - - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: 'CHOKLADKAKA' }) - await waitForElementToBeRemoved(() => screen.getByText(/searching/i)) - - expect(screen.getByText(/Search results/i)).toBeInTheDocument() - expect(screen.getByText(/An unknown error occurred - failed to retrieve course data./i)).toBeInTheDocument() - - expect(asFragment()).toMatchSnapshot() - }) -}) - -describe('Component and resolved cases', () => { - test('search by a text pattern to display at first a pending status message then the result of this search. English. 1B', async () => { - useStore.mockReturnValue({ - browserConfig: { proxyPrefixPath: { uri: '/student/kurser' } }, - language: 'en', - languageIndex: 0, - }) - - koppsCourseSearch.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - - const { asFragment } = render() - - expect(screen.getByText(/searching/i)).toBeInTheDocument() - - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: 'Algebra' }) - - await waitForElementToBeRemoved(() => screen.getByText(/searching/i)) - - expect(screen.getByTestId('number-of-results')).toHaveTextContent('Your search returned 2 result(s).') - - const rows = screen.queryAllByRole('row') - - expect(rows).toHaveLength(3) - expect(screen.getByText(/IX1303/i)).toBeInTheDocument() - expect(screen.getByText(/SF1624/i)).toBeInTheDocument() - - expect(asFragment()).toMatchSnapshot() - }) - - test('double search: by deparment&pattern and then by education level. English. 2B', async () => { - useStore.mockReturnValue({ - browserConfig: { proxyPrefixPath: { uri: '/student/kurser' } }, - language: 'en', - languageIndex: 0, - }) - - koppsCourseSearch.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - - const { asFragment, rerender } = render( - - ) - - expect(screen.getByText(/searching/i)).toBeInTheDocument() - - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: 'Algebra', department: 'ABD' }) - - await waitForElementToBeRemoved(() => screen.getByText(/searching/i)) - - expect(screen.getByTestId('number-of-results')).toHaveTextContent('Your search returned 2 result(s).') - - const rows = screen.queryAllByRole('row') - - expect(rows).toHaveLength(3) - expect(screen.getByText(/IX1303/i)).toBeInTheDocument() - expect(screen.getByText(/SF1624/i)).toBeInTheDocument() - - expect(asFragment()).toMatchSnapshot() - - // rerender, second search - - koppsCourseSearch.mockReturnValue(Promise.resolve(TEST_API_ANSWER_RESOLVED)) - - rerender( - - ) - - expect(screen.getByText(/searching/i)).toBeInTheDocument() - - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { - eduLevel: ['0', '1', '2'], - showOptions: ['onlyEnglish', 'showCancelled', 'onlyMHU'], - }) - - await waitForElementToBeRemoved(() => screen.getByText(/searching/i)) - - expect(screen.getByTestId('number-of-results')).toHaveTextContent('Your search returned 2 result(s).') - - const newrows = screen.queryAllByRole('row') - - expect(newrows).toHaveLength(3) - expect(screen.getByText(/AF2402/i)).toBeInTheDocument() - expect(screen.getByText(/AH2905/i)).toBeInTheDocument() - - expect(asFragment()).toMatchSnapshot() - }) -}) diff --git a/public/js/app/components/NewSearchResultDisplay/ListView.test.tsx b/public/js/app/components/SearchResultDisplay/ListView.test.tsx similarity index 100% rename from public/js/app/components/NewSearchResultDisplay/ListView.test.tsx rename to public/js/app/components/SearchResultDisplay/ListView.test.tsx diff --git a/public/js/app/components/NewSearchResultDisplay/ListView.tsx b/public/js/app/components/SearchResultDisplay/ListView.tsx similarity index 99% rename from public/js/app/components/NewSearchResultDisplay/ListView.tsx rename to public/js/app/components/SearchResultDisplay/ListView.tsx index 29a00000..a2ba3445 100644 --- a/public/js/app/components/NewSearchResultDisplay/ListView.tsx +++ b/public/js/app/components/SearchResultDisplay/ListView.tsx @@ -6,7 +6,7 @@ import { useStore } from '../../mobx' import { ListViewParams } from './types' import i18n from '../../../../../i18n' -import { compareCoursesBy, inforKursvalLink, periodsStr } from '../../util/newSearchHelper' +import { compareCoursesBy, inforKursvalLink, periodsStr } from '../../util/searchHelper' const ListView: React.FC = ({ results }) => { const { language, languageIndex } = useStore() diff --git a/public/js/app/components/NewSearchResultDisplay/SearchResultComponent.tsx b/public/js/app/components/SearchResultDisplay/SearchResultComponent.tsx similarity index 100% rename from public/js/app/components/NewSearchResultDisplay/SearchResultComponent.tsx rename to public/js/app/components/SearchResultDisplay/SearchResultComponent.tsx diff --git a/public/js/app/components/NewSearchResultDisplay/SearchResultDisplay.test.tsx b/public/js/app/components/SearchResultDisplay/SearchResultDisplay.test.tsx similarity index 82% rename from public/js/app/components/NewSearchResultDisplay/SearchResultDisplay.test.tsx rename to public/js/app/components/SearchResultDisplay/SearchResultDisplay.test.tsx index d14ab0a0..74be7205 100644 --- a/public/js/app/components/NewSearchResultDisplay/SearchResultDisplay.test.tsx +++ b/public/js/app/components/SearchResultDisplay/SearchResultDisplay.test.tsx @@ -1,7 +1,7 @@ import React from 'react' import { render, screen, waitFor } from '@testing-library/react' import '@testing-library/jest-dom' -import NewSearchResultDisplay from './index' +import SearchResultDisplay from './index' import { useStore } from '../../mobx' import { TEST_API_ANSWER_NO_HITS, @@ -10,7 +10,7 @@ import { } from '../mocks/mockKoppsCourseSearch' import { ERROR_ASYNC, STATUS } from '../../hooks/types/UseCourseSearchTypes' jest.mock('../../mobx') -describe('NewSearchResultDisplay component', () => { +describe('SearchResultDisplay component', () => { beforeEach(() => { ;(useStore as jest.Mock).mockReturnValue({ languageIndex: 0 }) }) @@ -22,7 +22,7 @@ describe('NewSearchResultDisplay component', () => { error: null as any, } - render() + render() await waitFor(() => { expect(screen.getByText('Standard')).toBeInTheDocument() @@ -36,7 +36,7 @@ describe('NewSearchResultDisplay component', () => { error: ERROR_ASYNC.rejected, } - render() + render() await waitFor(() => { expect(screen.getByText(/An unknown error occurred - failed to retrieve course data/i)).toBeInTheDocument() }) @@ -49,7 +49,7 @@ describe('NewSearchResultDisplay component', () => { error: null as any, } - render() + render() expect(screen.getByText('Searching ...')).toBeInTheDocument() }) @@ -61,7 +61,7 @@ describe('NewSearchResultDisplay component', () => { error: ERROR_ASYNC.noHits, } - render() + render() expect(screen.getByText('Your search returned no results')).toBeInTheDocument() }) @@ -69,7 +69,7 @@ describe('NewSearchResultDisplay component', () => { test('displays no search params message', () => { const resultsState = { data: null as null, status: STATUS.noQueryProvided, error: ERROR_ASYNC.noQueryProvided } - render() + render() expect(screen.getByText('No query restriction was specified')).toBeInTheDocument() }) diff --git a/public/js/app/components/NewSearchResultDisplay/SearchResultHeader.tsx b/public/js/app/components/SearchResultDisplay/SearchResultHeader.tsx similarity index 100% rename from public/js/app/components/NewSearchResultDisplay/SearchResultHeader.tsx rename to public/js/app/components/SearchResultDisplay/SearchResultHeader.tsx diff --git a/public/js/app/components/NewSearchResultDisplay/TableView.test.tsx b/public/js/app/components/SearchResultDisplay/TableView.test.tsx similarity index 86% rename from public/js/app/components/NewSearchResultDisplay/TableView.test.tsx rename to public/js/app/components/SearchResultDisplay/TableView.test.tsx index 34d1f560..16918998 100644 --- a/public/js/app/components/NewSearchResultDisplay/TableView.test.tsx +++ b/public/js/app/components/SearchResultDisplay/TableView.test.tsx @@ -4,14 +4,14 @@ import '@testing-library/jest-dom' import TableView from './TableView' import { useStore } from '../../mobx' import { - EXPECTED_TEST_SEARCH_HITS_MIXED_EN_BETA, - EXPECTED_TEST_SEARCH_HITS_MIXED_SV_BETA, - TEST_SEARCH_HITS_MIXED_EN_BETA, - TEST_SEARCH_HITS_MIXED_SV_BETA, - TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV_BETA, - TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN_BETA, - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN_BETA, - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV_BETA, + EXPECTED_TEST_SEARCH_HITS_MIXED_EN, + EXPECTED_TEST_SEARCH_HITS_MIXED_SV, + TEST_SEARCH_HITS_MIXED_EN, + TEST_SEARCH_HITS_MIXED_SV, + TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV, + TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN, + EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN, + EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV, } from '../mocks/mockSearchHits' jest.mock('../../mobx') @@ -32,7 +32,7 @@ describe('Component for RESEARCH courses', () => { test('creates a table with 4 columns for RESEARCH courses (without column for period intervals). English. 1A', () => { ;(useStore as jest.Mock).mockReturnValue({ language: 'en', languageIndex: 0 }) - render() + render() const rows = screen.queryAllByRole('row') const columnHeaders = screen.getAllByRole('columnheader') @@ -45,7 +45,7 @@ describe('Component for RESEARCH courses', () => { rows.slice(1).forEach((row, index) => { const utils = within(row) - const course = TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN_BETA.searchHits[index] + const course = TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN.searchHits[index] expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.kod) expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.benamning) expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.omfattning.formattedWithUnit}`) @@ -56,7 +56,7 @@ describe('Component for RESEARCH courses', () => { test('creates a table with 4 columns for RESEARCH courses (without column for period intervals). Swedish. 2A', () => { ;(useStore as jest.Mock).mockReturnValue({ language: 'sv', languageIndex: 1 }) - render() + render() const rows = screen.queryAllByRole('row') const columnHeaders = screen.getAllByRole('columnheader') @@ -69,7 +69,7 @@ describe('Component for RESEARCH courses', () => { rows.slice(1).forEach((row, index) => { const utils = within(row) - const course = TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV_BETA.searchHits[index] + const course = TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV.searchHits[index] expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.kod) expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.benamning) expect(utils.getAllByRole('cell')[2]).toHaveTextContent(course.omfattning.formattedWithUnit) @@ -82,7 +82,7 @@ describe('Component for MIXED types of courses', () => { test('creates a table with 5 columns for mixed types of courses (including column for period intervals). English. 1B', () => { ;(useStore as jest.Mock).mockReturnValue({ language: 'en', languageIndex: 0 }) - render() + render() const rows = screen.queryAllByRole('row') const columnHeaders = screen.getAllByRole('columnheader') @@ -95,7 +95,7 @@ describe('Component for MIXED types of courses', () => { rows.slice(1).forEach((row, index) => { const utils = within(row) - const course = EXPECTED_TEST_SEARCH_HITS_MIXED_EN_BETA.searchHits[index] + const course = EXPECTED_TEST_SEARCH_HITS_MIXED_EN.searchHits[index] expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.kod) expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.benamning) expect(utils.getAllByRole('cell')[2]).toHaveTextContent(course.omfattning.formattedWithUnit) @@ -104,7 +104,7 @@ describe('Component for MIXED types of courses', () => { expect(utils.getAllByRole('cell')[5]).toHaveTextContent(`${course.studietakt[0].code}%`) expect(utils.getAllByRole('cell')[6]).toHaveTextContent(course.studieort[0].name) expect(utils.getAllByRole('cell')[7]).toHaveTextContent( - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN_BETA[index] + EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN[index] ) }) }) @@ -112,7 +112,7 @@ describe('Component for MIXED types of courses', () => { test('creates a table with 8 columns for MIXED types of courses (including column for period intervals). Swedish. 2B', () => { ;(useStore as jest.Mock).mockReturnValue({ language: 'sv', languageIndex: 1 }) - render() + render() const rows = screen.queryAllByRole('row') const columnHeaders = screen.getAllByRole('columnheader') @@ -125,7 +125,7 @@ describe('Component for MIXED types of courses', () => { rows.slice(1).forEach((row, index) => { const utils = within(row) - const course = EXPECTED_TEST_SEARCH_HITS_MIXED_SV_BETA.searchHits[index] + const course = EXPECTED_TEST_SEARCH_HITS_MIXED_SV.searchHits[index] expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.kod) expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.benamning) expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.omfattning.formattedWithUnit}`) @@ -134,7 +134,7 @@ describe('Component for MIXED types of courses', () => { expect(utils.getAllByRole('cell')[5]).toHaveTextContent(`${course.studietakt[0].code}%`) expect(utils.getAllByRole('cell')[6]).toHaveTextContent(course.studieort[0].name) expect(utils.getAllByRole('cell')[7]).toHaveTextContent( - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV_BETA[index] + EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV[index] ) }) }) diff --git a/public/js/app/components/NewSearchResultDisplay/TableView.tsx b/public/js/app/components/SearchResultDisplay/TableView.tsx similarity index 98% rename from public/js/app/components/NewSearchResultDisplay/TableView.tsx rename to public/js/app/components/SearchResultDisplay/TableView.tsx index b1ca63a5..03664838 100644 --- a/public/js/app/components/NewSearchResultDisplay/TableView.tsx +++ b/public/js/app/components/SearchResultDisplay/TableView.tsx @@ -5,7 +5,7 @@ import './style.scss' import { useStore } from '../../mobx' import translate from '../../../../../domain/translate' -import { sortAndParseByCourseCodeForTableView } from '../../util/newSearchHelper' +import { sortAndParseByCourseCodeForTableView } from '../../util/searchHelper' import { TableViewParams } from './types' import { SortableTable } from '@kth/kth-reactstrap/dist/components/studinfo' diff --git a/public/js/app/components/NewSearchResultDisplay/index.tsx b/public/js/app/components/SearchResultDisplay/index.tsx similarity index 85% rename from public/js/app/components/NewSearchResultDisplay/index.tsx rename to public/js/app/components/SearchResultDisplay/index.tsx index df4d9f14..d1f62c81 100644 --- a/public/js/app/components/NewSearchResultDisplay/index.tsx +++ b/public/js/app/components/SearchResultDisplay/index.tsx @@ -15,7 +15,7 @@ import { AlertType } from '../SearchAlert/types' const isCourseSearchResult = (data: string | CourseSearchResult): data is CourseSearchResult => { return (data as CourseSearchResult).searchHits !== undefined } -const NewSearchResultDisplay: React.FC = ({ resultsState }) => { +const SearchResultDisplay: React.FC = ({ resultsState }) => { const { languageIndex } = useStore() const [view, setView] = useState(VIEW.list) const { data: searchResults, status: searchStatus, error: errorType } = resultsState @@ -23,7 +23,7 @@ const NewSearchResultDisplay: React.FC = ({ resultsSt if (errorType) { return ( ) @@ -44,4 +44,4 @@ const NewSearchResultDisplay: React.FC = ({ resultsSt ) } -export default NewSearchResultDisplay +export default SearchResultDisplay diff --git a/public/js/app/components/NewSearchResultDisplay/style.scss b/public/js/app/components/SearchResultDisplay/style.scss similarity index 100% rename from public/js/app/components/NewSearchResultDisplay/style.scss rename to public/js/app/components/SearchResultDisplay/style.scss diff --git a/public/js/app/components/NewSearchResultDisplay/types.ts b/public/js/app/components/SearchResultDisplay/types.ts similarity index 100% rename from public/js/app/components/NewSearchResultDisplay/types.ts rename to public/js/app/components/SearchResultDisplay/types.ts diff --git a/public/js/app/components/SearchTableView.jsx b/public/js/app/components/SearchTableView.jsx deleted file mode 100644 index ca06d06a..00000000 --- a/public/js/app/components/SearchTableView.jsx +++ /dev/null @@ -1,164 +0,0 @@ -import React from 'react' - -import { Col, Row } from 'reactstrap' -import PropTypes from 'prop-types' - -import { Link, SortableTable } from '@kth/kth-reactstrap/dist/components/studinfo' -import { useStore } from '../mobx' - -import translate from '../../../../domain/translate' -import i18n from '../../../../i18n' - -import { courseLink } from '../util/links' -import { formatShortTerm } from '../../../../domain/term' -import Article from './Article' -import { translateCreditUnitAbbr } from '../util/translateCreditUnitAbbr' - -function codeCell(code, startTerm) { - const { language } = useStore() - - return { - content: {code}, - sortKey: code, - } -} - -function titleCell(code, title, startTerm) { - const { language } = useStore() - return { - content: {title}, - sortKey: title, - } -} - -function compareCoursesBy(key) { - return function compare(a, b) { - if (a[key] < b[key]) { - return -1 - } - if (a[key] > b[key]) { - return 1 - } - return 0 - } -} - -function periodsStr(startPeriod, startTerm, endPeriod, endTerm) { - const { language } = useStore() - // P5 VT21 - // P5 spring 21 - P0 autumn 21 - if (!startTerm || !startPeriod.toString()) return '' - if (!endTerm || !endPeriod.toString()) return `P${startPeriod} ${formatShortTerm(startTerm, language)}` - if (startPeriod === endPeriod && startTerm === endTerm) - return `P${startPeriod} ${formatShortTerm(startTerm, language)}` - return `P${startPeriod} ${formatShortTerm(startTerm, language)} - P${endPeriod} ${formatShortTerm(endTerm, language)}` -} - -function sortAndParseByCourseCode(courses, languageIndex, language, sliceUntilNum) { - const { bigSearch } = i18n.messages[languageIndex] - courses.sort(compareCoursesBy('courseCode')) - const parsedCourses = courses.map( - ({ - courseCode: code, - title, - credits, - creditUnitAbbr, - educationalLevel: level, - startPeriod, - startTerm, - endPeriod, - endTerm, - }) => - [ - codeCell(code, startTerm), - titleCell(code, title, startTerm), - `${credits} ${translateCreditUnitAbbr(language, creditUnitAbbr)}`, - bigSearch[level] || '', - periodsStr(startPeriod, startTerm, endPeriod, endTerm) || '', - ].slice(0, sliceUntilNum) - ) - return parsedCourses -} - -const SearchTableView = ({ unsortedSearchResults }) => { - const { searchHits } = unsortedSearchResults - - if (!searchHits) return null - - const { language, languageIndex } = useStore() - - const t = translate(language) - - let hasSearchHitInterval = false - - const flatCoursesArr = searchHits.map(({ course, searchHitInterval }) => { - if (searchHitInterval) hasSearchHitInterval = true - return { - ...course, - ...searchHitInterval, - } - }) - - const sliceUntilNum = hasSearchHitInterval ? 5 : 4 - const headers = [ - t('course_code'), - t('course_name'), - t('course_scope'), - t('course_educational_level'), - t('department_period_abbr'), - ].slice(0, sliceUntilNum) - - const courses = sortAndParseByCourseCode(flatCoursesArr, languageIndex, language, sliceUntilNum) - - const hitsNumber = courses.length - return ( - - -
- {language === 'en' ? ( -

- {'Your search returned '} - {hitsNumber} - {' result(s).'} -

- ) : ( -

- {`Din sökning gav `} - {hitsNumber} - {' resultat.'} -

- )} - -
- -
- ) -} - -export const searchHitsPropsShape = { - searchHits: PropTypes.arrayOf( - PropTypes.shape({ - course: PropTypes.shape({ - courseCode: PropTypes.string, - creditUnitAbbr: PropTypes.string, - credits: PropTypes.number, - educationalLevel: PropTypes.string, - title: PropTypes.string, - }).isRequired, - searchHitInterval: PropTypes.shape({ - endPeriod: PropTypes.number, - endTerm: PropTypes.string, - startPeriod: PropTypes.number, - startTerm: PropTypes.string, - }), - }) - ), -} -SearchTableView.propTypes = { - unsortedSearchResults: PropTypes.shape(searchHitsPropsShape), -} - -SearchTableView.defaultProps = { - unsortedSearchResults: {}, -} -export default SearchTableView diff --git a/public/js/app/components/SearchTableView.test.js b/public/js/app/components/SearchTableView.test.js deleted file mode 100644 index 8e7f33c8..00000000 --- a/public/js/app/components/SearchTableView.test.js +++ /dev/null @@ -1,271 +0,0 @@ -import React from 'react' -import { render, screen, within } from '@testing-library/react' -import '@testing-library/jest-dom' -import SearchTableView from './SearchTableView' -import { useStore } from '../mobx' -import { - EXPECTED_TEST_SEARCH_HITS_MIXED_EN, - EXPECTED_TEST_SEARCH_HITS_MIXED_SV, - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN, - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV, - TEST_SEARCH_HITS_MIXED_EN, - TEST_SEARCH_HITS_MIXED_SV, - TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV, - TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN, -} from './mocks/mockSearchHits' - -jest.mock('../mobx') - -const reseacrhHitsColHeaders = { - en: ['Course code', 'Course name', 'Scope', 'Educational level'], - sv: ['Kurskod', 'Kursnamn', 'Omfattning', 'Utbildningsnivå'], -} -const mixedHitsColHeaders = { - en: [...reseacrhHitsColHeaders.en, 'Periods'], - sv: [...reseacrhHitsColHeaders.sv, 'Perioder'], -} -const eduLevelTranslations = { - EN: { PREPARATORY: 'Pre-university level', BASIC: 'First cycle', ADVANCED: 'Second cycle', RESEARCH: 'Third cycle' }, - SV: { PREPARATORY: 'Förberedande nivå', BASIC: 'Grundnivå', ADVANCED: 'Avancerad nivå', RESEARCH: 'Forskarnivå' }, -} - -describe('Component for RESEARCH courses', () => { - test('creates a table with 4 columns for RESEARCH courses (without column for period intervals). English. 1A', () => { - useStore.mockReturnValue({ language: 'en', languageIndex: 0 }) - const { asFragment } = render( - - ) - - expect(asFragment()).toMatchSnapshot() - // or more detailed - expect(screen.getByTestId('number-of-results')).toHaveTextContent('Your search returned 3 result(s).') - - const rows = screen.queryAllByRole('row') - const columnHeaders = screen.getAllByRole('columnheader') - const contentCells = screen.queryAllByRole('cell') - expect(rows).toHaveLength(4) - expect(columnHeaders).toHaveLength(4) - expect(contentCells).toHaveLength(12) - - try { - columnHeaders.map((colHeader, index) => expect(colHeader).toHaveTextContent(reseacrhHitsColHeaders.en[index])) - } catch (error) { - error.message = `${`Table head row missing a correct translations. List of correct translations ${reseacrhHitsColHeaders.en.join( - ', ' - )}`}\n\n ${error}` - - throw error - } - - try { - rows.slice(1).forEach((row, index) => { - const utils = within(row) - const { course } = TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN.searchHits[index] - expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.courseCode, { exact: true }) - expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.title, { exact: true }) - expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.credits} credits`, { - exact: true, - }) - expect(utils.getAllByRole('cell')[3]).toHaveTextContent('Third cycle') - }) - } catch (error) { - error.message = `${`Courses has been rendered incorrect. Course content in the table is differ from an expected content`}\n\n ${error}` - - throw error - } - }) - - test('creates a table with 4 columns for RESEARCH courses (without column for period intervals). Swedish. 2A', () => { - useStore.mockReturnValue({ language: 'sv', languageIndex: 1 }) - const { asFragment } = render( - - ) - - expect(asFragment()).toMatchSnapshot() - // // or - // or more detailed - expect(screen.getByTestId('number-of-results')).toHaveTextContent('Din sökning gav 3 resultat.') - - const rows = screen.queryAllByRole('row') - const columnHeaders = screen.getAllByRole('columnheader') - const contentCells = screen.queryAllByRole('cell') - expect(rows).toHaveLength(4) - expect(columnHeaders).toHaveLength(4) - expect(contentCells).toHaveLength(12) - - try { - columnHeaders.map((colHeader, index) => expect(colHeader).toHaveTextContent(reseacrhHitsColHeaders.sv[index])) - } catch (error) { - error.message = `${`Table head row missing a correct translations. List of correct translations ${reseacrhHitsColHeaders.sv.join( - ', ' - )}`}\n\n ${error}` - - throw error - } - - try { - rows.slice(1).forEach((row, index) => { - const utils = within(row) - const { course } = TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV.searchHits[index] - expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.courseCode, { exact: true }) - expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.title, { exact: true }) - expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.credits} ${course.creditUnitAbbr}`, { - exact: true, - }) - expect(utils.getAllByRole('cell')[3]).toHaveTextContent('Forskarnivå') - }) - } catch (error) { - error.message = `${`Courses has been rendered incorrect. Course content in the table is differ from an expected content`}\n\n ${error}` - - throw error - } - }) - - test('creates a table with 5 columns for mixed types of courses (include column for period intervals). English. 1B', () => { - useStore.mockReturnValue({ language: 'en', languageIndex: 0 }) - const { asFragment } = render( - - ) - - expect(asFragment()).toMatchSnapshot() - // or more detailed - expect(screen.getByTestId('number-of-results')).toHaveTextContent('Your search returned 3 result(s).') - - const rows = screen.queryAllByRole('row') - const columnHeaders = screen.getAllByRole('columnheader') - const contentCells = screen.queryAllByRole('cell') - expect(rows).toHaveLength(4) - expect(columnHeaders).toHaveLength(4) - expect(contentCells).toHaveLength(12) - - try { - columnHeaders.map((colHeader, index) => expect(colHeader).toHaveTextContent(reseacrhHitsColHeaders.en[index])) - } catch (error) { - error.message = `${`Table head row missing a correct translations. List of correct translations ${reseacrhHitsColHeaders.en.join( - ', ' - )}`}\n\n ${error}` - - throw error - } - - try { - rows.slice(1).forEach((row, index) => { - const utils = within(row) - const { course } = TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN.searchHits[index] - expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.courseCode, { exact: true }) - expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.title, { exact: true }) - expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.credits} credits`, { - exact: true, - }) - expect(utils.getAllByRole('cell')[3]).toHaveTextContent('Third cycle') - }) - } catch (error) { - error.message = `${`Courses has been rendered incorrect. Course content in the table is differ from an expected content`}\n\n ${error}` - - throw error - } - }) -}) - -describe('Component for MIXED types of courses', () => { - test('creates a table with 5 columns for mixed types of courses (include column for period intervals). English. 1B', () => { - useStore.mockReturnValue({ language: 'en', languageIndex: 0 }) - const { asFragment } = render() - - expect(asFragment()).toMatchSnapshot() - // or more detailed - expect(screen.getByTestId('number-of-results')).toHaveTextContent('Your search returned 5 result(s).') - - const rows = screen.queryAllByRole('row') - const columnHeaders = screen.getAllByRole('columnheader') - const contentCells = screen.queryAllByRole('cell') - expect(rows).toHaveLength(6) - expect(columnHeaders).toHaveLength(5) - expect(contentCells).toHaveLength(25) - - try { - columnHeaders.map((colHeader, index) => expect(colHeader).toHaveTextContent(mixedHitsColHeaders.en[index])) - } catch (error) { - error.message = `${`Table head row missing a correct translations. List of correct translations ${mixedHitsColHeaders.en.join( - ', ' - )}`}\n\n ${error}` - - throw error - } - - try { - rows.slice(1).forEach((row, index) => { - const utils = within(row) - const { course } = EXPECTED_TEST_SEARCH_HITS_MIXED_EN.searchHits[index] - expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.courseCode, { exact: true }) - expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.title, { exact: true }) - expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.credits} credits`, { - exact: true, - }) - expect(utils.getAllByRole('cell')[3]).toHaveTextContent( - course.educationalLevel ? eduLevelTranslations.EN[course.educationalLevel] : '', - { exact: true } - ) - expect(utils.getAllByRole('cell')[4]).toHaveTextContent( - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN[index], - { exact: true } - ) - }) - } catch (error) { - error.message = `${`Courses has been rendered incorrect. Course content in the table is differ from an expected content`}\n\n ${error}` - - throw error - } - }) - - test('creates a table with 5 columns for MIXED type of courses (include a column for period intervals). Swedish. 2B', () => { - useStore.mockReturnValue({ language: 'sv', languageIndex: 1 }) - const { asFragment } = render() - - expect(asFragment()).toMatchSnapshot() - // // or - // or more detailed - expect(screen.getByTestId('number-of-results')).toHaveTextContent('Din sökning gav 5 resultat.') - - const rows = screen.queryAllByRole('row') - const columnHeaders = screen.getAllByRole('columnheader') - const contentCells = screen.queryAllByRole('cell') - expect(rows).toHaveLength(6) - expect(columnHeaders).toHaveLength(5) - expect(contentCells).toHaveLength(25) - - try { - columnHeaders.map((colHeader, index) => expect(colHeader).toHaveTextContent(mixedHitsColHeaders.sv[index])) - } catch (error) { - error.message = `${`Table head row missing a correct translations. List of correct translations ${mixedHitsColHeaders.sv.join( - ', ' - )}`}\n\n ${error}` - - throw error - } - - try { - rows.slice(1).forEach((row, index) => { - const utils = within(row) - const { course } = EXPECTED_TEST_SEARCH_HITS_MIXED_SV.searchHits[index] - expect(utils.getAllByRole('cell')[0]).toHaveTextContent(course.courseCode, { exact: true }) - expect(utils.getAllByRole('cell')[1]).toHaveTextContent(course.title, { exact: true }) - expect(utils.getAllByRole('cell')[2]).toHaveTextContent(`${course.credits} ${course.creditUnitAbbr}`, { - exact: true, - }) - expect(utils.getAllByRole('cell')[3]).toHaveTextContent( - course.educationalLevel ? eduLevelTranslations.SV[course.educationalLevel] : '', - { exact: true } - ) - expect(utils.getAllByRole('cell')[4]).toHaveTextContent( - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV[index], - { exact: true } - ) - }) - } catch (error) { - error.message = `${`Courses has been rendered incorrectly. Course content in the table is differ from an expected content`}\n\n ${error}` - - throw error - } - }) -}) diff --git a/public/js/app/components/ThirdCycleStudySearchFormFields.jsx b/public/js/app/components/ThirdCycleStudySearchFormFields.jsx deleted file mode 100644 index 498225f9..00000000 --- a/public/js/app/components/ThirdCycleStudySearchFormFields.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useReducer } from 'react' -import { Col, Row } from 'reactstrap' -import PropTypes from 'prop-types' -import { CollapseDetails } from '@kth/kth-reactstrap/dist/components/utbildningsinfo' - -import { useStore } from '../mobx' -import i18n from '../../../../i18n' - -import SearchDepartments from './SearchDepartments' -import SearchOptions from './SearchOptions' - -const paramsReducer = (state, action) => ({ ...state, ...action }) - -function SearchFormFields({ caption, openOptions, onSubmit }) { - const { languageIndex, textPattern: initialPattern = '' } = useStore() - - const { generalSearch, bigSearch } = i18n.messages[languageIndex] - const { searchLabel, collapseHeaderOtherSearchOptions } = generalSearch - const { onlyMHULabel } = bigSearch - const [state, setState] = useReducer(paramsReducer, { pattern: initialPattern }) - const { pattern } = state - - function handlePatternChange(e) { - const { value } = e.target - const cleanTextPattern = value ? value.replace(/['"<>$]+/g, '') : '' - setState({ pattern: cleanTextPattern }) - } - - function handleSubmit(e) { - e.preventDefault() - - setState({ pattern: pattern.trim() }) - - onSubmit(state) - } - - function handleParamChange(params) { - setState(params) - } - - return ( -
-
- - -
- - - - - - - - - - - - - - - - - -
- ) -} - -SearchFormFields.propTypes = { - caption: PropTypes.string.isRequired, - onSubmit: PropTypes.func.isRequired, - openOptions: PropTypes.bool.isRequired, -} - -export default SearchFormFields diff --git a/public/js/app/components/__snapshots__/SearchResultDisplay.test.js.snap b/public/js/app/components/__snapshots__/SearchResultDisplay.test.js.snap deleted file mode 100644 index 8047cbef..00000000 --- a/public/js/app/components/__snapshots__/SearchResultDisplay.test.js.snap +++ /dev/null @@ -1,495 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Component and its warning messages double search: by a text pattern and rerender it again with the empty parameter to get a warning message noQueryProvided. English. 2A 1`] = ` - -

- Search results -

-

- - No query restriction was specified - -

-
-`; - -exports[`Component and its warning messages double search: show an overflow warning message and then show a message about empty results "noHits". English. 3A 1`] = ` - -

- Search results -

-

- - Your search returned no results. - -

-
-`; - -exports[`Component and its warning messages double search: show empty results message "noHits" and then show a rejected message of unknown error. English. 4A 1`] = ` - -

- Search results -

-

- - Your search returned no results. - -

-
-`; - -exports[`Component and its warning messages double search: show empty results message "noHits" and then show a rejected message of unknown error. English. 4A 2`] = ` - -

- Search results -

-

- - An unknown error occurred - failed to retrieve course data. - -

-
-`; - -exports[`Component and its warning messages double search: twice render empty parameters, first time without asking kopps, second time show a warning message. English. 1A 1`] = ``; - -exports[`Component and its warning messages double search: twice render empty parameters, first time without asking kopps, second time show a warning message. English. 1A 2`] = ` - -

- Search results -

-

- - No query restriction was specified - -

-
-`; - -exports[`Component and its warning messages search:show a rejected message of unknown error. English. 5A 1`] = ` - -

- Search results -

-

- - An unknown error occurred - failed to retrieve course data. - -

-
-`; - -exports[`Component and resolved cases double search: by deparment&pattern and then by education level. English. 2B 1`] = ` - -

- Search results -

-
-
- -
-
-
-`; - -exports[`Component and resolved cases double search: by deparment&pattern and then by education level. English. 2B 2`] = ` - -

- Search results -

-
-
- -
-
-
-`; - -exports[`Component and resolved cases search by a text pattern to display at first a pending status message then the result of this search. English. 1B 1`] = ` - -

- Search results -

-
-
- -
-
-
-`; diff --git a/public/js/app/components/__snapshots__/SearchTableView.test.js.snap b/public/js/app/components/__snapshots__/SearchTableView.test.js.snap deleted file mode 100644 index f0893670..00000000 --- a/public/js/app/components/__snapshots__/SearchTableView.test.js.snap +++ /dev/null @@ -1,863 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Component for MIXED types of courses creates a table with 5 columns for MIXED type of courses (include a column for period intervals). Swedish. 2B 1`] = ` - -
-
-
-

- Din sökning gav - - 5 - - resultat. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Kurskod - - - Kursnamn - - - Omfattning - - - Utbildningsnivå - - - Perioder - -
- - AF233X - - - - Examensarbete inom byggnadsmateriallära, avancerad nivå - - - 30 hp - - Avancerad nivå - - P1 HT21 - P2 HT21 -
- - AF2402 - - - - Akustik och brand - - - 7.5 hp - - Grundnivå - - P1 HT21 - P3 VT22 -
- - AH2905 - - - - Avancerad analys och design av vägbeläggningar - - - 7.5 hp - - - P1 HT21 -
- - FAF3901 - - - - Avancerad reologi för bituminösa material - - - 7.5 hp - - Forskarnivå - -
- - FAH3904 - - - - Introduktion till asfaltskemin - - - 4 hp - - Förberedande nivå - - P0 HT21 - P5 VT22 -
-
-
-
-
-`; - -exports[`Component for MIXED types of courses creates a table with 5 columns for mixed types of courses (include column for period intervals). English. 1B 1`] = ` - -
-
-
-

- Your search returned - - 5 - - result(s). -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Course code - - - Course name - - - Scope - - - Educational level - - - Periods - -
- - AF233X - - - - Degree Project in Building Materials, Second Cycle - - - 30 credits - - Second cycle - - P1 Autumn 21 - P2 Autumn 21 -
- - AF2402 - - - - Acoustics and Fire - - - 7.5 credits - - First cycle - - P1 Autumn 21 - P3 Spring 22 -
- - AH2905 - - - - Advanced Pavement Engineering Analysis and Design - - - 7.5 credits - - - P1 Autumn 21 -
- - FAF3901 - - - - Advanced Rheology of Bituminous Materials - - - 7.5 credits - - Third cycle - -
- - FAH3904 - - - - Introduction to Asphalt Chemistry - - - 4 credits - - Pre-university level - - P0 Autumn 21 - P5 Spring 22 -
-
-
-
-
-`; - -exports[`Component for RESEARCH courses creates a table with 4 columns for RESEARCH courses (without column for period intervals). English. 1A 1`] = ` - -
-
-
-

- Your search returned - - 3 - - result(s). -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Course code - - - Course name - - - Scope - - - Educational level - -
- - FAF3302 - - - - Project in Building Materials Technology - - - 7.5 credits - - Third cycle -
- - FAF3304 - - - - Wood Chemistry, Biocomposites and Building Materials - - - 7.5 credits - - Third cycle -
- - FAF3305 - - - - Pavement Design and Performance Prediction - - - 3 credits - - Third cycle -
-
-
-
-
-`; - -exports[`Component for RESEARCH courses creates a table with 4 columns for RESEARCH courses (without column for period intervals). Swedish. 2A 1`] = ` - -
-
- -
-
-
-`; - -exports[`Component for RESEARCH courses creates a table with 5 columns for mixed types of courses (include column for period intervals). English. 1B 1`] = ` - -
-
-
-

- Your search returned - - 3 - - result(s). -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Course code - - - Course name - - - Scope - - - Educational level - -
- - FAF3302 - - - - Project in Building Materials Technology - - - 7.5 credits - - Third cycle -
- - FAF3304 - - - - Wood Chemistry, Biocomposites and Building Materials - - - 7.5 credits - - Third cycle -
- - FAF3305 - - - - Pavement Design and Performance Prediction - - - 3 credits - - Third cycle -
-
-
-
-
-`; diff --git a/public/js/app/components/index.js b/public/js/app/components/index.js index ab777aac..0015ab31 100644 --- a/public/js/app/components/index.js +++ b/public/js/app/components/index.js @@ -1,31 +1,10 @@ -import SearchInputField from './SearchInputField' import SearchAlert from './SearchAlert' -import NewSearchOptions from './NewSearchOptions' -import NewSearchDepartments from './NewSearchDepartments' import SearchOptions from './SearchOptions' import SearchDepartments from './SearchDepartments' import HelpTexts from './HelpTexts' import Lead from './Lead' import FooterContent from './FooterContent' -import SearchResultDisplay from './SearchResultDisplay' -import ThirdCycleStudySearchFormFields from './ThirdCycleStudySearchFormFields' -import SearchFormFields from './SearchFormFields' import SearchFilters from './SearchFilters' import SearchInput from './SearchInput' -export { - SearchAlert, - SearchDepartments, - NewSearchDepartments, - SearchInputField, - SearchOptions, - NewSearchOptions, - HelpTexts, - Lead, - FooterContent, - SearchResultDisplay, - ThirdCycleStudySearchFormFields, - SearchFormFields, - SearchFilters, - SearchInput, -} +export { SearchAlert, SearchDepartments, SearchOptions, HelpTexts, Lead, FooterContent, SearchFilters, SearchInput } diff --git a/public/js/app/components/mocks/mockSearchHits.ts b/public/js/app/components/mocks/mockSearchHits.ts index 6ef84534..49ba7fad 100644 --- a/public/js/app/components/mocks/mockSearchHits.ts +++ b/public/js/app/components/mocks/mockSearchHits.ts @@ -1,88 +1,5 @@ // F.e., ?term_period=20212%3A1&department_prefix=AFB&flag=in_english_only const TEST_SEARCH_HITS_MIXED_EN = { - // UNSORTED - searchHits: [ - { - // BASIC - course: { - courseCode: 'AF2402', - title: 'Acoustics and Fire', - credits: 7.5, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - educationalLevel: 'BASIC', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20221', - startPeriod: 1, - endPeriod: 3, - }, - }, - { - // NO education level - course: { - courseCode: 'AH2905', - title: 'Advanced Pavement Engineering Analysis and Design', - credits: 7.5, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20212', - startPeriod: 1, - endPeriod: 1, - }, - }, - { - // RESEARCH, NO searchHitInterval - course: { - courseCode: 'FAF3901', - title: 'Advanced Rheology of Bituminous Materials', - credits: 7.5, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - educationalLevel: 'RESEARCH', - }, - }, - { - // ADVANCED - course: { - courseCode: 'AF233X', - title: 'Degree Project in Building Materials, Second Cycle', - credits: 30, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - educationalLevel: 'ADVANCED', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20212', - startPeriod: 1, - endPeriod: 2, - }, - }, - { - // PREPARATORY - course: { - courseCode: 'FAH3904', - title: 'Introduction to Asphalt Chemistry', - credits: 4, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - educationalLevel: 'PREPARATORY', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20221', - startPeriod: 0, - endPeriod: 5, - }, - }, - ], -} -const TEST_SEARCH_HITS_MIXED_EN_BETA = { searchHits: [ { kod: 'AF2402', @@ -478,91 +395,8 @@ const TEST_SEARCH_HITS_MIXED_EN_BETA = { ], } -const TEST_SEARCH_HITS_MIXED_SV = { - // UNSORTED - searchHits: [ - { - // BASIC - course: { - courseCode: 'AF2402', - title: 'Akustik och brand', - credits: 7.5, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - educationalLevel: 'BASIC', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20221', - startPeriod: 1, - endPeriod: 3, - }, - }, - { - // NO education level - course: { - courseCode: 'AH2905', - title: 'Avancerad analys och design av vägbeläggningar', - credits: 7.5, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20212', - startPeriod: 1, - endPeriod: 1, - }, - }, - { - // RESEARCH, NO searchHitInterval - course: { - courseCode: 'FAF3901', - title: 'Avancerad reologi för bituminösa material', - credits: 7.5, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - educationalLevel: 'RESEARCH', - }, - }, - { - // ADVANCED - course: { - courseCode: 'AF233X', - title: 'Examensarbete inom byggnadsmateriallära, avancerad nivå', - credits: 30, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - educationalLevel: 'ADVANCED', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20212', - startPeriod: 1, - endPeriod: 2, - }, - }, - { - // PREPARATORY - course: { - courseCode: 'FAH3904', - title: 'Introduktion till asfaltskemin', - credits: 4, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - educationalLevel: 'PREPARATORY', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20221', - startPeriod: 0, - endPeriod: 5, - }, - }, - ], -} -const TEST_SEARCH_HITS_MIXED_SV_BETA = { +const TEST_SEARCH_HITS_MIXED_SV = { searchHits: [ { kod: 'AF2402', @@ -950,123 +784,24 @@ const TEST_SEARCH_HITS_MIXED_SV_BETA = { ], } -const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN = [ - 'P1 Autumn 21 - P2 Autumn 21', - 'P1 Autumn 21 - P3 Spring 22', - 'P1 Autumn 21', - '', - 'P0 Autumn 21 - P5 Spring 22', -] // todo: this needs to be deleted after removing the old search -const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN_BETA = [ +const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN = [ 'P1 Autumn 21 - P2 Autumn 21', 'P1 Autumn 21 - P3 Spring 22', 'P1 Autumn 21', 'Missing', 'P0 Autumn 21 - P5 Spring 22', ] -const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV = [ - 'P1 HT21 - P2 HT21', - 'P1 HT21 - P3 VT22', - 'P1 HT21', - '', - 'P0 HT21 - P5 VT22', -] -const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV_BETA = [ +const EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV = [ 'P1 HT21 - P2 HT21', 'P1 HT21 - P3 VT22', 'P1 HT21', 'Saknas', 'P0 HT21 - P5 VT22', -] // todo: this needs to be deleted after removing the old search +] const EXPECTED_TEST_SEARCH_HITS_MIXED_EN = { - // SORTED - searchHits: [ - { - // ADVANCED - course: { - courseCode: 'AF233X', - title: 'Degree Project in Building Materials, Second Cycle', - credits: 30, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - educationalLevel: 'ADVANCED', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20212', - startPeriod: 1, - endPeriod: 2, - }, - }, - { - // BASIC - course: { - courseCode: 'AF2402', - title: 'Acoustics and Fire', - credits: 7.5, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - educationalLevel: 'BASIC', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20221', - startPeriod: 1, - endPeriod: 3, - }, - }, - { - // NO education level - course: { - courseCode: 'AH2905', - title: 'Advanced Pavement Engineering Analysis and Design', - credits: 7.5, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20212', - startPeriod: 1, - endPeriod: 1, - }, - }, - { - // RESEARCH, NO searchHitInterval - course: { - courseCode: 'FAF3901', - title: 'Advanced Rheology of Bituminous Materials', - credits: 7.5, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - educationalLevel: 'RESEARCH', - }, - }, - - { - // PREPARATORY - course: { - courseCode: 'FAH3904', - title: 'Introduction to Asphalt Chemistry', - credits: 4, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - educationalLevel: 'PREPARATORY', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20221', - startPeriod: 0, - endPeriod: 5, - }, - }, - ], -} - -const EXPECTED_TEST_SEARCH_HITS_MIXED_EN_BETA = { searchHits: [ { kod: 'AF233X', @@ -1455,91 +1190,6 @@ const EXPECTED_TEST_SEARCH_HITS_MIXED_EN_BETA = { } const EXPECTED_TEST_SEARCH_HITS_MIXED_SV = { - // SORTED - searchHits: [ - { - // ADVANCED - course: { - courseCode: 'AF233X', - title: 'Examensarbete inom byggnadsmateriallära, avancerad nivå', - credits: 30, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - educationalLevel: 'ADVANCED', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20212', - startPeriod: 1, - endPeriod: 2, - }, - }, - { - // BASIC - course: { - courseCode: 'AF2402', - title: 'Akustik och brand', - credits: 7.5, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - educationalLevel: 'BASIC', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20221', - startPeriod: 1, - endPeriod: 3, - }, - }, - { - // NO education level - course: { - courseCode: 'AH2905', - title: 'Avancerad analys och design av vägbeläggningar', - credits: 7.5, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20212', - startPeriod: 1, - endPeriod: 1, - }, - }, - { - // RESEARCH, NO searchHitInterval - course: { - courseCode: 'FAF3901', - title: 'Avancerad reologi för bituminösa material', - credits: 7.5, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - educationalLevel: 'RESEARCH', - }, - }, - - { - // PREPARATORY - course: { - courseCode: 'FAH3904', - title: 'Introduktion till asfaltskemin', - credits: 4, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - educationalLevel: 'PREPARATORY', - }, - searchHitInterval: { - startTerm: '20212', - endTerm: '20221', - startPeriod: 0, - endPeriod: 5, - }, - }, - ], -} - -const EXPECTED_TEST_SEARCH_HITS_MIXED_SV_BETA = { searchHits: [ { kod: 'AF233X', @@ -1928,42 +1578,6 @@ const EXPECTED_TEST_SEARCH_HITS_MIXED_SV_BETA = { } const TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV = { - // RESEARCH, NO searchHitInterval - searchHits: [ - { - course: { - courseCode: 'FAF3302', - title: 'Projekt i byggnadsmaterialteknik', - credits: 7.5, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - educationalLevel: 'RESEARCH', - }, - }, - { - course: { - courseCode: 'FAF3304', - title: 'Träkemi för biokompositer som byggnadsmaterial', - credits: 7.5, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - educationalLevel: 'RESEARCH', - }, - }, - { - course: { - courseCode: 'FAF3305', - title: 'Vägdimensionering och prestandautvärdering', - credits: 3, - creditUnitLabel: 'Högskolepoäng', - creditUnitAbbr: 'hp', - educationalLevel: 'RESEARCH', - }, - }, - ], -} - -const TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV_BETA = { searchHits: [ { kod: 'FAF3302', @@ -2125,42 +1739,6 @@ const TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV_BETA = { } const TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN = { - // RESEARCH, NO searchHitInterval - searchHits: [ - { - course: { - courseCode: 'FAF3302', - title: 'Project in Building Materials Technology', - credits: 7.5, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - educationalLevel: 'RESEARCH', - }, - }, - { - course: { - courseCode: 'FAF3304', - title: 'Wood Chemistry, Biocomposites and Building Materials', - credits: 7.5, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - educationalLevel: 'RESEARCH', - }, - }, - { - course: { - courseCode: 'FAF3305', - title: 'Pavement Design and Performance Prediction', - credits: 3, - creditUnitLabel: 'Credits', - creditUnitAbbr: 'hp', - educationalLevel: 'RESEARCH', - }, - }, - ], -} - -const TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN_BETA = { searchHits: [ { kod: 'FAF3302', @@ -2323,19 +1901,11 @@ const TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN_BETA = { export { EXPECTED_TEST_SEARCH_HITS_MIXED_EN, - EXPECTED_TEST_SEARCH_HITS_MIXED_EN_BETA, EXPECTED_TEST_SEARCH_HITS_MIXED_SV, - EXPECTED_TEST_SEARCH_HITS_MIXED_SV_BETA, EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN, - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_EN_BETA, EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV, - EXPECTED_TEST_SEARCH_HITS_MIXED_PERIODS_TEXTS_SV_BETA, TEST_SEARCH_HITS_MIXED_EN, - TEST_SEARCH_HITS_MIXED_EN_BETA, TEST_SEARCH_HITS_MIXED_SV, - TEST_SEARCH_HITS_MIXED_SV_BETA, TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV, - TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_SV_BETA, TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN, - TEST_SEARCH_RESEARCH_THIRD_CYCLE_COURSES_EN_BETA, } diff --git a/public/js/app/config/menuData.js b/public/js/app/config/menuData.js index d84aaa9f..6f60c275 100644 --- a/public/js/app/config/menuData.js +++ b/public/js/app/config/menuData.js @@ -5,7 +5,7 @@ const { throwErrorIfNoBrowserConfig } = require('../util/errors') function getMenuData(applicationStore) { const { language, browserConfig } = applicationStore throwErrorIfNoBrowserConfig(browserConfig) - const { programmesList, courseSearch, department, studyHandbook, newSearchPage } = browserConfig.proxyPrefixPath + const { programmesList, department, studyHandbook, searchPage } = browserConfig.proxyPrefixPath const t = translate(language) return { ariaLabel: t('main_menu_aria_label'), @@ -27,18 +27,8 @@ function getMenuData(applicationStore) { id: 'searchAllCourses', type: 'leaf', text: t('main_menu_search_all'), - url: pageLink(courseSearch), + url: pageLink(searchPage), }, - ...(applicationStore.isNewSearch - ? [ - { - id: 'searchAllCourses-new', - type: 'leaf', - text: t('main_menu_search_all_new'), - url: pageLink(newSearchPage), - }, - ] - : []), { id: 'departmentsList', type: 'leaf', diff --git a/public/js/app/config/thirdCycleMenuData.js b/public/js/app/config/thirdCycleMenuData.js index 9e91fe65..caf93d39 100644 --- a/public/js/app/config/thirdCycleMenuData.js +++ b/public/js/app/config/thirdCycleMenuData.js @@ -6,8 +6,7 @@ function getThirdCycleMenuData(applicationStore) { const { language, browserConfig } = applicationStore throwErrorIfNoBrowserConfig(browserConfig) - const { thirdCycleSchoolsAndDepartments, thirdCycleCourseSearch, thirdCycleCourseSearchNew } = - browserConfig.proxyPrefixPath + const { thirdCycleSchoolsAndDepartments, thirdCycleCourseSearch } = browserConfig.proxyPrefixPath const t = translate(language) return { ariaLabel: t('main_menu_aria_label'), @@ -31,16 +30,6 @@ function getThirdCycleMenuData(applicationStore) { text: t('main_menu_third_cycle_courses_search'), url: pageLink(thirdCycleCourseSearch, language), }, - ...(applicationStore.isNewSearch - ? [ - { - id: 'searchThirdCycleCoursesNew', - type: 'leaf', - text: t('main_menu_third_cycle_courses_search_new'), - url: pageLink(thirdCycleCourseSearchNew, language), - }, - ] - : []), ], }, } diff --git a/public/js/app/hooks/useCourseSearchParams.ts b/public/js/app/hooks/useCourseSearchParams.ts index 2e15dd3f..20b4dc2b 100644 --- a/public/js/app/hooks/useCourseSearchParams.ts +++ b/public/js/app/hooks/useCourseSearchParams.ts @@ -22,14 +22,14 @@ export const useCourseSearchParams = (): [CourseSearchParams, SetCourseSearchPar const setCourseSearchParams = (updatedCourseSearchParam: Partial) => { // merge current courseSearchParams with updated value and remove empty values - const newSearchParams = Object.fromEntries( + const searchParams = Object.fromEntries( Object.entries({ ...courseSearchParams, ...updatedCourseSearchParam, }).filter(([, value]) => !!value) ) - setSearchParams(newSearchParams) + setSearchParams(searchParams) } return [courseSearchParams, setCourseSearchParams] diff --git a/public/js/app/pages/CourseSearch.jsx b/public/js/app/pages/CourseSearch.jsx deleted file mode 100644 index 12e24198..00000000 --- a/public/js/app/pages/CourseSearch.jsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState } from 'react' -import { Col, Row } from 'reactstrap' -import Alert from '../components-shared/Alert' -import { CollapseDetails } from '@kth/kth-reactstrap/dist/components/utbildningsinfo' -import { PageHeading, Link } from '@kth/kth-reactstrap/dist/components/studinfo' - -import { useStore } from '../mobx' -import i18n from '../../../../i18n' -import { Lead, FooterContent, SearchResultDisplay, SearchFormFields, HelpTexts } from '../components' - -import { getHelpText, hasValue, openOptionsInCollapse } from '../util/searchHelper' -import { pageLink } from '../util/links' - -function _checkAndGetCollapseOptions({ department, eduLevel, period, showOptions }) { - // clean params - - const optionsValues = {} - - if (hasValue(eduLevel)) optionsValues.eduLevel = eduLevel - if (hasValue(showOptions)) optionsValues.showOptions = showOptions - if (hasValue(period)) optionsValues.period = period - if (hasValue(department)) optionsValues.department = department - - return optionsValues -} - -function _checkAndGetResultValues({ department, eduLevel, pattern, period, showOptions }) { - // clean params - const optionsInCollapse = _checkAndGetCollapseOptions({ department, eduLevel, period, showOptions }) - - const resultValues = optionsInCollapse - if (hasValue(pattern)) resultValues.pattern = pattern - - return resultValues -} - -const CourseSearch = () => { - const { - languageIndex, - language, - textPattern: storePattern, - departmentCodeOrPrefix: storeDepartment, - eduLevel: storeEduLevel, - period: storePeriod, - showOptions: storeShowOptions, - } = useStore() - const { bigSearch, searchInstructions } = i18n.messages[languageIndex] - const { searchHeading, leadIntro, searchButton } = bigSearch - const { - search_help_collapse_header: collapseHeader, - beta_version_title, - beta_version_description, - beta_version_link, - } = searchInstructions - // const [loadStatus, setLoadStatus] = useState('firstLoad') - const [params, setParams] = useState( - _checkAndGetResultValues({ - department: storeDepartment, - eduLevel: storeEduLevel, - pattern: storePattern, - period: storePeriod, - showOptions: storeShowOptions, - }) - ) - - const helptexts = getHelpText(languageIndex, 'searchInstructions', [ - 'search_help_1', - 'search_help_2', - 'search_help_3', - 'search_help_4', - 'search_help_5', - 'search_help_6', - 'search_help_7', - 'search_help_8', - 'search_help_9', - 'search_help_10', - ]) - - function handleSubmit(props) { - const finalSearchParams = _checkAndGetResultValues(props) - - setParams(finalSearchParams) - } - - return ( - <> - - - {searchHeading} - - -

{beta_version_description}

- - {beta_version_link} -
- -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} - -export default CourseSearch diff --git a/public/js/app/pages/CourseSearch.test.js b/public/js/app/pages/CourseSearch.test.js deleted file mode 100644 index 355d9b97..00000000 --- a/public/js/app/pages/CourseSearch.test.js +++ /dev/null @@ -1,196 +0,0 @@ -import React from 'react' -import { fireEvent, render, screen, act, waitFor, waitForElementToBeRemoved, within } from '@testing-library/react' -import '@testing-library/jest-dom' -import koppsCourseSearch from '../util/internApi' -import CourseSearch from './CourseSearch' -import { useStore } from '../mobx' -import { TEST_API_ANSWER_ALGEBRA, TEST_API_ANSWER_RESOLVED } from '../components/mocks/mockKoppsCourseSearch' - -const mockDate = new Date('2021-03-23 16:00') - -jest.setTimeout(1000) - -jest.mock('../mobx') -jest.mock('../util/internApi') -jest.mock('react-router-dom', () => ({ - useNavigate: jest.fn(), -})) -const periods = [ - 'Spring 2021 period 3', - 'Spring 2021 period 4', - '2021 summer', - 'Autumn 2021 period 1', - 'Autumn 2021 period 2', - 'Spring 2022 period 3', - 'Spring 2022 period 4', - '2022 summer', -] -const eduLevels = ['Pre-university level', 'First cycle', 'Second cycle', 'Third cycle'] -const showOptions = [ - 'Courses taught in English', - 'Courses that deal with environment, environmental technology or sustainable development', - 'Dormant/Terminated course', -] - -describe('Component , events', () => { - beforeAll(() => { - jest.spyOn(global, 'Date').mockImplementation(() => mockDate) - }) - - afterAll(() => { - jest.spyOn(global, 'Date').mockRestore() - }) - - // search options - test('get all empty checkboxes then check them all and get a result, rerender and get the same result', async () => { - useStore.mockReturnValue({ - browserConfig: { proxyPrefixPath: { uri: '/student/kurser' } }, - language: 'en', - languageIndex: 0, - currentSchoolsWithDepartments: [], - deprecatedSchoolsWithDepartments: [], - textPattern: '', - departmentCodeOrPrefix: '', - eduLevel: [], - period: [], - showOptions: [], - }) - koppsCourseSearch.mockReturnValue(Promise.resolve(TEST_API_ANSWER_RESOLVED)) - - const { rerender } = render() - - expect(koppsCourseSearch).not.toHaveBeenCalled() - periods.forEach(label => { - const checkbox = screen.getByLabelText(label) - expect(checkbox).not.toBeChecked() - - fireEvent.click(checkbox) - expect(checkbox).toBeChecked() - }) - - eduLevels.forEach(label => { - const checkbox = screen.getByLabelText(label) - expect(checkbox).not.toBeChecked() - - fireEvent.click(checkbox) - expect(checkbox).toBeChecked() - }) - - showOptions.forEach(label => { - const checkbox = screen.getByLabelText(label) - expect(checkbox).not.toBeChecked() - - fireEvent.click(checkbox) - expect(checkbox).toBeChecked() - }) - - const button = screen.getByRole('button', { - name: /search course/i, - }) - - fireEvent.click(button) - await waitFor(() => expect(screen.getByText('Searching ...')).toBeInTheDocument()) - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { - eduLevel: ['0', '1', '2', '3'], - period: ['20211:3', '20211:4', '2021:summer', '20212:1', '20212:2', '20221:3', '20221:4', '2022:summer'], - showOptions: ['onlyEnglish', 'onlyMHU', 'showCancelled'], - }) - - expect(screen.getByTestId('number-of-results')).toHaveTextContent('Your search returned 2 result(s).') - const newrows = screen.queryAllByRole('row') - - expect(newrows).toHaveLength(3) - expect(screen.getByText(/AF2402/i)).toBeInTheDocument() - expect(screen.getByText(/AH2905/i)).toBeInTheDocument() - - act(() => rerender()) - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { - eduLevel: ['0', '1', '2', '3'], - period: ['20211:3', '20211:4', '2021:summer', '20212:1', '20212:2', '20221:3', '20221:4', '2022:summer'], - showOptions: ['onlyEnglish', 'onlyMHU', 'showCancelled'], - }) - expect(screen.getByTestId('number-of-results')).toHaveTextContent('Your search returned 2 result(s).') - }, 5000) - - // search options - test('get periods checkboxes checked and then test uncheck it', async () => { - const periodValues = [ - '20211:3', - '20211:4', - '2021:summer', - '20212:1', - '20212:2', - '20221:3', - '20221:4', - '2022:summer', - ] - useStore.mockReturnValue({ - browserConfig: { proxyPrefixPath: { uri: '/student/kurser' } }, - language: 'en', - languageIndex: 0, - currentSchoolsWithDepartments: [], - deprecatedSchoolsWithDepartments: [], - textPattern: '', - departmentCodeOrPrefix: '', - eduLevel: [], - period: periodValues, - showOptions: [], - }) - koppsCourseSearch.mockReturnValue(Promise.resolve(TEST_API_ANSWER_RESOLVED)) - - render() - - expect(screen.getByText('Searching ...')).toBeInTheDocument() - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { - period: ['20211:3', '20211:4', '2021:summer', '20212:1', '20212:2', '20221:3', '20221:4', '2022:summer'], - }) - await waitForElementToBeRemoved(() => screen.getByText('Searching ...')) - - expect(screen.getByTestId('number-of-results')).toHaveTextContent('Your search returned 2 result(s).') - const newrows = screen.queryAllByRole('row') - - expect(newrows).toHaveLength(3) - expect(screen.getByText(/AF2402/i)).toBeInTheDocument() - expect(screen.getByText(/AH2905/i)).toBeInTheDocument() - - eduLevels.forEach(label => { - const checkbox = screen.getByLabelText(label) - expect(checkbox).not.toBeChecked() - }) - - periods.forEach(label => { - const checkbox = screen.getByLabelText(label) - expect(checkbox).toBeChecked() - - fireEvent.click(checkbox) - expect(checkbox).not.toBeChecked() - }) - - showOptions.forEach(label => { - const checkbox = screen.getByLabelText(label) - expect(checkbox).not.toBeChecked() - }) - - const button = screen.getByRole('button', { - name: /search course/i, - }) - - koppsCourseSearch.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - - fireEvent.click(button) - await waitFor(() => expect(screen.getByText('Searching ...')).toBeInTheDocument()) - - await waitFor(() => - expect(koppsCourseSearch).toHaveBeenCalledWith('en', '/student/kurser', { - period: [], - }) - ) - - expect(screen.getByTestId('number-of-results')).toHaveTextContent('Your search returned 2 result(s).') - const nrows = screen.queryAllByRole('row') - const thirdRow = within(nrows[2]) - expect(nrows).toHaveLength(3) - expect(screen.getByText(/IX1303/i)).toBeInTheDocument() - expect(thirdRow.getByText(/SF1624/i)).toBeInTheDocument() - }) -}) diff --git a/public/js/app/pages/CourseSearchLayout.test.js b/public/js/app/pages/CourseSearchLayout.test.js deleted file mode 100644 index efc98140..00000000 --- a/public/js/app/pages/CourseSearchLayout.test.js +++ /dev/null @@ -1,408 +0,0 @@ -import React from 'react' -import { render, screen, fireEvent, within } from '@testing-library/react' -import { toHaveNoViolations } from 'jest-axe' -import { axe } from './test-config/axeWithoutLandmarkUniqueRule' -import '@testing-library/jest-dom' -import { StaticRouter } from 'react-router-dom/server' -import { MobxStoreProvider } from '../mobx' - -import CourseSearch from './CourseSearch' -import ElementWrapper from '../components/ElementWrapper' -import PageLayout from '../layout/PageLayout' - -import createApplicationStore from '../stores/createApplicationStore' -import getMenuData from '../config/menuData' - -import commonSettings from '../config/mocks/mockCommonSettings' - -expect.extend(toHaveNoViolations) - -const storeId = 'searchCourses' -const applicationStore = createApplicationStore(storeId) - -const WrappedCourseSearch = ({ lang, ...rest }) => { - applicationStore.setLanguage(lang) - applicationStore.setBrowserConfig(commonSettings) - - const updatedApplicationStore = { - ...applicationStore, - } - const initApplicationStoreCallback = () => updatedApplicationStore - return ( - - ({ selectedId: 'searchAllCourses', ...getMenuData(store) })} - // eslint-disable-next-line react/jsx-props-no-spreading - {...rest} - /> - - ) -} - -const CourseSearchWithLayout = ({ lang }) => { - applicationStore.setLanguage(lang) - applicationStore.setBrowserConfig(commonSettings) - const menuData = getMenuData(applicationStore) - - const updatedApplicationStore = { - ...applicationStore, - } - return ( - - updatedApplicationStore}> - - - - - - ) -} - -describe('Render component CourseSearch within RouterWrapper', () => { - test('simple render', async () => { - render() - }) -}) -const mockDate = new Date('2021-01-23 16:00') - -describe('Render component CourseSearch within Layout', () => { - beforeAll(() => { - jest.spyOn(global, 'Date').mockImplementation(() => mockDate) - }) - - afterAll(() => { - jest.spyOn(global, 'Date').mockRestore() - }) - - test('get page header in English', () => { - render() - const h1Header = screen.getByRole('heading', { level: 1 }) - expect(h1Header).toHaveTextContent('Search courses') - }) - - test('get page header in Swedish', () => { - render() - const h1Header = screen.getByRole('heading', { level: 1 }) - expect(h1Header).toHaveTextContent('Sök kurser') - }) - - test('match to snapshot in English', async () => { - const { asFragment, container } = render() - const results = await axe(container, { - rules: { - 'heading-order': { enabled: false }, - }, - }) - expect(results).toHaveNoViolations() - - expect(asFragment()).toMatchSnapshot() - }) - - test('match to snapshot in Swedish', async () => { - const { asFragment, container } = render() - const results = await axe(container, { - rules: { - 'heading-order': { enabled: false }, - }, - }) - expect(results).toHaveNoViolations() - - expect(asFragment()).toMatchSnapshot() - }) -}) - -describe('Render component CourseSearch and check its menu, content and links', () => { - beforeAll(() => { - jest.spyOn(global, 'Date').mockImplementation(() => mockDate) - }) - - afterAll(() => { - jest.spyOn(global, 'Date').mockRestore() - }) - test('check all links on the page in English', () => { - render() - const links = screen.getAllByRole('link') - expect(links.length).toBe(9) - expect(links[0]).toHaveTextContent('Studies') - expect(links[0].href).toStrictEqual('http://localhost/student/studier/?l=en') - - expect(links[1]).toHaveTextContent('Programme Syllabuses') - expect(links[1].href).toStrictEqual('http://localhost/student/kurser/kurser-inom-program') - - expect(links[2]).toHaveTextContent('Search courses') - expect(links[2].href).toStrictEqual('http://localhost/student/kurser/sokkurs') - - expect(links[3]).toHaveTextContent('Courses by school') - expect(links[3].href).toStrictEqual('http://localhost/student/kurser/org') - - expect(links[4]).toHaveTextContent('Studies before 07/08') // menu link - expect(links[4].href).toStrictEqual('http://localhost/student/program/shb') - - expect(links[5]).toHaveTextContent('Search courses (beta)') - expect(links[5].href).toStrictEqual('http://localhost/student/kurser/sokkurs-beta?l=en') - - expect(links[6]).toHaveTextContent('kopps@kth.se') // address in search instructions, link - expect(links[6].href).toStrictEqual('mailto:kopps@kth.se') - - expect(links[7]).toHaveTextContent('Central study counseling') - // expect(links[6].href).toStrictEqual('https://www.kth.se/studycounselling') - - expect(links[8]).toHaveTextContent('kopps@kth.se') - expect(links[8].href).toStrictEqual('mailto:kopps@kth.se') - }) - - test('check all links on the page in Swedish', () => { - render() - const links = screen.getAllByRole('link') - expect(links.length).toBe(9) - expect(links[0]).toHaveTextContent('Studier') - expect(links[0].href).toStrictEqual('http://localhost/student/studier/') - - expect(links[1]).toHaveTextContent('Utbildningsplaner') - expect(links[1].href).toStrictEqual('http://localhost/student/kurser/kurser-inom-program') - - expect(links[2]).toHaveTextContent('Sök kurser') - expect(links[2].href).toStrictEqual('http://localhost/student/kurser/sokkurs') - - expect(links[3]).toHaveTextContent('Kurser per skola') - expect(links[3].href).toStrictEqual('http://localhost/student/kurser/org') - - expect(links[4]).toHaveTextContent('Studier före 07/08') // menu link - expect(links[4].href).toStrictEqual('http://localhost/student/program/shb') - - expect(links[5]).toHaveTextContent('Sök kurser (beta)') - expect(links[5].href).toStrictEqual('http://localhost/student/kurser/sokkurs-beta') - - expect(links[6]).toHaveTextContent('kopps@kth.se') // address in search instructions, link - expect(links[6].href).toStrictEqual('mailto:kopps@kth.se') - - expect(links[7]).toHaveTextContent('Central studievägledning') - // expect(links[6].href).toStrictEqual('https://www.kth.se/studycounselling') - - expect(links[8]).toHaveTextContent('kopps@kth.se') - expect(links[8].href).toStrictEqual('mailto:kopps@kth.se') - }) - - test('get page introduction in English', () => { - render() - const content = screen.getByText( - 'Find info on KTH courses: course syllabus, course memo, and course analyses. Search by course name or course code, you can also filter by semester and period. Which courses are included in a program can be found under Programme syllabuses.' - ) - expect(content).toBeInTheDocument() - }) - - test('get page introduction in Swedish', () => { - render() - const content = screen.getByText( - 'Här finns info om kurser på KTH: kursplan, kurs-PM och kursanalyser. Sök på kursnamn eller kurskod, du kan även filtrera på termin och läsperiod. Vilka kurser som ingår i ett program finns under Utbildningsplaner.' - ) - expect(content).toBeInTheDocument() - }) - - test('get a label of a text pattern input in English', () => { - render() - const content = screen.getByText('Search by course name or course code') - expect(content).toBeInTheDocument() - }) - - test('get a label of a text pattern input in Swedish', () => { - render() - const content = screen.getByText(`Sök på kursnamn eller kurskod`) - expect(content).toBeInTheDocument() - }) - - test('get a label of the collapse with other options in English', () => { - render() - const content = screen.getByText('Filter your search choices') - expect(content).toBeInTheDocument() - }) - - test('get a label of the collapse with other options in Swedish', () => { - render() - const content = screen.getByText(`Filtrera dina sökval`) - expect(content).toBeInTheDocument() - }) - - // button - test('get a search button in English', () => { - render() - const mainContent = screen.getByRole('main') - const button = within(mainContent).getByRole('button') - expect(button).toHaveTextContent('Search course', { exact: true }) - }) - - test('get a search button in Swedish', () => { - render() - const mainContent = screen.getByRole('main') - const button = within(mainContent).getByRole('button') - expect(button).toHaveTextContent('Sök kurs', { exact: true }) - }) - - // search options - test('get labels of search options in English', () => { - render() - - expect(screen.getByText(`Course Start 2021`)).toBeInTheDocument() - expect(screen.getByText(`Spring 2021 period 3`)).toBeInTheDocument() - expect(screen.getByText(`Spring 2021 period 4`)).toBeInTheDocument() - expect(screen.getByText(`2021 summer`)).toBeInTheDocument() - expect(screen.getByText(`Autumn 2021 period 1`)).toBeInTheDocument() - expect(screen.getByText(`Autumn 2021 period 2`)).toBeInTheDocument() - - expect(screen.getByText(`Course Start 2022`)).toBeInTheDocument() - expect(screen.getByText(`Spring 2022 period 3`)).toBeInTheDocument() - expect(screen.getByText(`Spring 2022 period 4`)).toBeInTheDocument() - expect(screen.getByText(`2022 summer`)).toBeInTheDocument() - - expect(screen.getByText(`Educational level:`)).toBeInTheDocument() - expect(screen.getByText(`Pre-university level`)).toBeInTheDocument() - expect(screen.getByText(`First cycle`)).toBeInTheDocument() - expect(screen.getByText(`Second cycle`)).toBeInTheDocument() - expect(screen.getByText(`Third cycle`)).toBeInTheDocument() - - expect(screen.getByText(`Other options:`)).toBeInTheDocument() - expect(screen.getByText(`Courses taught in English`)).toBeInTheDocument() - expect( - screen.getByText(`Courses that deal with environment, environmental technology or sustainable development`) - ).toBeInTheDocument() - expect(screen.getByText(`Dormant/Terminated course`)).toBeInTheDocument() - - expect(screen.getByText(`School, department, etc`)).toBeInTheDocument() - }) - - test('get labels of search options in Swedish', () => { - render() - - expect(screen.getByText(`Kursstart 2021`)).toBeInTheDocument() - expect(screen.getByText(`VT2021 period 3`)).toBeInTheDocument() - expect(screen.getByText(`VT2021 period 4`)).toBeInTheDocument() - expect(screen.getByText(`2021 sommar`)).toBeInTheDocument() - expect(screen.getByText(`HT2021 period 1`)).toBeInTheDocument() - expect(screen.getByText(`HT2021 period 2`)).toBeInTheDocument() - - expect(screen.getByText(`Kursstart 2022`)).toBeInTheDocument() - expect(screen.getByText(`VT2022 period 3`)).toBeInTheDocument() - expect(screen.getByText(`VT2022 period 4`)).toBeInTheDocument() - expect(screen.getByText(`2022 sommar`)).toBeInTheDocument() - - expect(screen.getByText(`Utbildningsnivå:`)).toBeInTheDocument() - expect(screen.getByText(`Förberedande nivå`)).toBeInTheDocument() - expect(screen.getByText(`Grundnivå`)).toBeInTheDocument() - expect(screen.getByText(`Avancerad nivå`)).toBeInTheDocument() - expect(screen.getByText(`Forskarnivå`)).toBeInTheDocument() - - expect(screen.getByText(`Övrigt:`)).toBeInTheDocument() - expect(screen.getByText(`Ges på engelska`)).toBeInTheDocument() - expect(screen.getByText(`Behandlar miljö, miljöteknik eller hållbar utveckling`)).toBeInTheDocument() - expect(screen.getByText(`Nedlagd kurs`)).toBeInTheDocument() - fireEvent.click(screen.getByLabelText(/Nedlagd kurs/i)) - - expect(screen.getByText(`Skola, avdelning, etc`)).toBeInTheDocument() - }) - - // search options and its values - test('get values of search options in English', () => { - render() - - expect(screen.getByLabelText(/Spring 2021 period 3/i).value).toBe('20211:3') - expect(screen.getByLabelText(/Spring 2021 period 4/i).value).toBe('20211:4') - expect(screen.getByLabelText(/2021 summer/i).value).toBe('2021:summer') - expect(screen.getByLabelText(/Autumn 2021 period 1/i).value).toBe('20212:1') - expect(screen.getByLabelText(/Autumn 2021 period 2/i).value).toBe('20212:2') - - expect(screen.getByLabelText(/Spring 2022 period 3/i).value).toBe('20221:3') - expect(screen.getByLabelText(/Spring 2022 period 4/i).value).toBe('20221:4') - expect(screen.getByLabelText(/2022 summer/i).value).toBe('2022:summer') - - expect(screen.getByLabelText(/Pre-university level/i).value).toBe('0') - expect(screen.getByLabelText(/First cycle/i).value).toBe('1') - expect(screen.getByLabelText(/Second cycle/i).value).toBe('2') - expect(screen.getByLabelText(/Third cycle/i).value).toBe('3') - - expect(screen.getByLabelText(/Courses taught in English/i).value).toBe('onlyEnglish') - expect( - screen.getByLabelText(/Courses that deal with environment, environmental technology or sustainable development/i) - .value - ).toBe('onlyMHU') - expect(screen.getByLabelText(/Dormant\/Terminated course/i).value).toBe('showCancelled') - }) - - test('get values of search options in English', () => { - render() - - expect(screen.getByLabelText(/VT2021 period 3/i).value).toBe('20211:3') - expect(screen.getByLabelText(/VT2021 period 4/i).value).toBe('20211:4') - expect(screen.getByLabelText(/2021 sommar/i).value).toBe('2021:summer') - expect(screen.getByLabelText(/HT2021 period 1/i).value).toBe('20212:1') - expect(screen.getByLabelText(/HT2021 period 2/i).value).toBe('20212:2') - - expect(screen.getByLabelText(/VT2022 period 3/i).value).toBe('20221:3') - expect(screen.getByLabelText(/VT2022 period 4/i).value).toBe('20221:4') - expect(screen.getByLabelText(/2022 sommar/i).value).toBe('2022:summer') - - expect(screen.getByLabelText(/Förberedande nivå/i).value).toBe('0') - expect(screen.getByLabelText(/Grundnivå/i).value).toBe('1') - expect(screen.getByLabelText(/Avancerad nivå/i).value).toBe('2') - expect(screen.getByLabelText(/Forskarnivå/i).value).toBe('3') - - expect(screen.getByLabelText(/Ges på engelska/i).value).toBe('onlyEnglish') - expect(screen.getByLabelText(/Behandlar miljö, miljöteknik eller hållbar utveckling/i).value).toBe('onlyMHU') - expect(screen.getByLabelText(/Nedlagd kurs/i).value).toBe('showCancelled') - }) - - // search options and its checked status - test('get checked status of checkboxes for search options in English', () => { - render() - - expect(screen.getByLabelText(/Spring 2021 period 3/i)).not.toBeChecked() - expect(screen.getByLabelText(/Spring 2021 period 4/i)).not.toBeChecked() - expect(screen.getByLabelText(/2021 summer/i)).not.toBeChecked() - expect(screen.getByLabelText(/Autumn 2021 period 1/i)).not.toBeChecked() - expect(screen.getByLabelText(/Autumn 2021 period 2/i)).not.toBeChecked() - - expect(screen.getByLabelText(/Spring 2022 period 3/i)).not.toBeChecked() - expect(screen.getByLabelText(/Spring 2022 period 4/i)).not.toBeChecked() - expect(screen.getByLabelText(/2022 summer/i)).not.toBeChecked() - - expect(screen.getByLabelText(/Pre-university level/i)).not.toBeChecked() - expect(screen.getByLabelText(/First cycle/i)).not.toBeChecked() - expect(screen.getByLabelText(/Second cycle/i)).not.toBeChecked() - expect(screen.getByLabelText(/Third cycle/i)).not.toBeChecked() - - expect(screen.getByLabelText(/Courses taught in English/i)).not.toBeChecked() - expect( - screen.getByLabelText(/Courses that deal with environment, environmental technology or sustainable development/i) - ).not.toBeChecked() - expect(screen.getByLabelText(/Dormant\/Terminated course/i)).not.toBeChecked() - }) - - test('get checked status of checkboxes for search options in Swedish', () => { - render() - - expect(screen.getByLabelText(/Spring 2021 period 3/i)).not.toBeChecked() - expect(screen.getByLabelText(/Spring 2021 period 4/i)).not.toBeChecked() - expect(screen.getByLabelText(/2021 summer/i)).not.toBeChecked() - expect(screen.getByLabelText(/Autumn 2021 period 1/i)).not.toBeChecked() - expect(screen.getByLabelText(/Autumn 2021 period 2/i)).not.toBeChecked() - - expect(screen.getByLabelText(/Spring 2022 period 3/i)).not.toBeChecked() - expect(screen.getByLabelText(/Spring 2022 period 4/i)).not.toBeChecked() - expect(screen.getByLabelText(/2022 summer/i)).not.toBeChecked() - - expect(screen.getByLabelText(/Pre-university level/i)).not.toBeChecked() - expect(screen.getByLabelText(/First cycle/i)).not.toBeChecked() - expect(screen.getByLabelText(/Second cycle/i)).not.toBeChecked() - expect(screen.getByLabelText(/Third cycle/i)).not.toBeChecked() - - expect(screen.getByLabelText(/Courses taught in English/i)).not.toBeChecked() - expect( - screen.getByLabelText(/Courses that deal with environment, environmental technology or sustainable development/i) - ).not.toBeChecked() - expect(screen.getByLabelText(/Dormant\/Terminated course/i)).not.toBeChecked() - }) -}) diff --git a/public/js/app/pages/CourseSearchThirdCycleStudy.jsx b/public/js/app/pages/CourseSearchThirdCycleStudy.jsx deleted file mode 100644 index 6fa25c73..00000000 --- a/public/js/app/pages/CourseSearchThirdCycleStudy.jsx +++ /dev/null @@ -1,138 +0,0 @@ -import React, { useState } from 'react' -import { Col, Row } from 'reactstrap' -import { CollapseDetails } from '@kth/kth-reactstrap/dist/components/utbildningsinfo' -import { PageHeading, Link } from '@kth/kth-reactstrap/dist/components/studinfo' -import Alert from '../components-shared/Alert' - -import { useStore } from '../mobx' -import i18n from '../../../../i18n' -import { Lead, HelpTexts, FooterContent, SearchResultDisplay, ThirdCycleStudySearchFormFields } from '../components' - -import { replaceSiteLinkForThirdCyclePages, courseSearchLink, pageLink } from '../util/links' -import { getHelpText, hasValue, openOptionsInCollapse } from '../util/searchHelper' - -function _checkAndGetCollapseOptions({ department, showOptions }) { - // clean params - - const optionsValues = {} - - if (hasValue(showOptions)) optionsValues.showOptions = showOptions - if (hasValue(department)) optionsValues.department = department - - return optionsValues -} - -function _checkAndGetResultValues({ department, pattern, showOptions }) { - // clean params - const optionsInCollapse = _checkAndGetCollapseOptions({ department, showOptions }) - - const resultValues = optionsInCollapse - if (hasValue(pattern)) resultValues.pattern = pattern - if (hasValue(pattern) || Object.keys(optionsInCollapse).length !== 0) resultValues.eduLevel = ['3'] - - return resultValues -} - -const CourseSearchThirdCycleStudy = () => { - const { - language: lang, - languageIndex, - textPattern: storePattern, - departmentCodeOrPrefix: storeDepartment, - showOptions: storeShowOptions, - } = useStore() - const { thirdCycleSearch, thirdCycleSearchInstructions, messages } = i18n.messages[languageIndex] - const { searchHeading, leadIntro, linkToUsualSearch } = thirdCycleSearch - const { - search_help_collapse_header: collapseHeader, - beta_version_title, - beta_version_description, - beta_version_link, - } = thirdCycleSearchInstructions - - const [params, setParams] = useState( - _checkAndGetResultValues({ - department: storeDepartment, - pattern: storePattern, - showOptions: storeShowOptions, - }) - ) - - const helptexts = getHelpText(languageIndex, 'thirdCycleSearchInstructions', [ - 'search_research_help_1', - 'search_research_help_2', - 'search_research_help_3', - 'search_research_help_4', - 'search_research_help_5', - 'search_research_help_6', - ]) - - function handleSubmit(props) { - const finalSearchParams = _checkAndGetResultValues(props) - - setParams(finalSearchParams) - } - - React.useEffect(() => { - let isMounted = true - if (isMounted) { - const { third_cycle_courses_by_school: siteName } = messages - replaceSiteLinkForThirdCyclePages(siteName, lang) - } - return () => (isMounted = false) - }) - - return ( - <> - - - {searchHeading} - - -

{beta_version_description}

- - {beta_version_link} -
- -
- - -
- - - - - - - - - - - - - - - - - - - - - - {linkToUsualSearch} - - - - - - - - - ) -} - -export default CourseSearchThirdCycleStudy diff --git a/public/js/app/pages/SearchLandingPage.test.tsx b/public/js/app/pages/SearchLandingPage.test.tsx index ec1c3b7c..883f1b33 100644 --- a/public/js/app/pages/SearchLandingPage.test.tsx +++ b/public/js/app/pages/SearchLandingPage.test.tsx @@ -1,16 +1,15 @@ import React from 'react' import { fireEvent, render, screen } from '@testing-library/react' import '@testing-library/jest-dom' -import NewSearchLandingPage from './NewSearchLandingPage' +import SearchLandingPage from './SearchLandingPage' import { useStore } from '../mobx' import { useNavigate } from 'react-router-dom' -import { Mock } from 'node:test' import { stringifyUrlParams } from '../../../../domain/searchParams' const mockDate = new Date('2024-08-19 16:00') jest.mock('../mobx') -jest.mock('../util/internApi') +jest.mock('../util/searchApi') jest.mock('react-router-dom', () => ({ useNavigate: jest.fn(), useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]), @@ -23,7 +22,7 @@ const showOptions = [ 'Dormant/Terminated course', ] -describe('', () => { +describe('', () => { const mockNavigate = jest.fn() beforeAll(() => { @@ -44,20 +43,20 @@ describe('', () => { // Scenario 1: Basic search without any filters selected test('should perform a basic search without any filters selected', async () => { - render() + render() const button = screen.getByRole('button', { name: /search course/i }) fireEvent.click(button) expect(mockNavigate).toHaveBeenCalledWith({ - pathname: '/student/kurser/sokkurs-beta/resultat', + pathname: '/student/kurser/sokkurs/resultat', search: '?', }) }) // Scenario 2: Selecting all filters and submitting the form test('should select all filters and submit search', async () => { - render() + render() periods.forEach(label => { const checkbox = screen.getByLabelText(label) @@ -87,14 +86,14 @@ describe('', () => { }) expect(mockNavigate).toHaveBeenCalledWith({ - pathname: '/student/kurser/sokkurs-beta/resultat', + pathname: '/student/kurser/sokkurs/resultat', search: `?${searchParams}`, }) }) // Scenario 3: Unselecting a filter and submitting the form test('should unselect a filter and submit search', async () => { - render() + render() const firstPeriodCheckbox = screen.getByLabelText(periods[0]) fireEvent.click(firstPeriodCheckbox) // Check it @@ -113,14 +112,14 @@ describe('', () => { }) expect(mockNavigate).toHaveBeenCalledWith({ - pathname: '/student/kurser/sokkurs-beta/resultat', + pathname: '/student/kurser/sokkurs/resultat', search: `?${searchParams}`, }) }) // Scenario 4: Third Cycle Courses Mode test('should handle thirdCycleCourses mode and submit search', async () => { - render() + render() const button = screen.getByRole('button', { name: /search course/i }) fireEvent.click(button) @@ -132,14 +131,14 @@ describe('', () => { }) expect(mockNavigate).toHaveBeenCalledWith({ - pathname: '/utbildning/forskarutbildning/kurser/sok-beta/resultat', + pathname: '/utbildning/forskarutbildning/kurser/sok/resultat', search: `?${searchParams}`, }) }) // Scenario 5: Navigating with specific parameters test('should navigate to correct result page with specific parameters', async () => { - render() + render() const firstPeriodCheckbox = screen.getByLabelText(periods[0]) fireEvent.click(firstPeriodCheckbox) // Select a period @@ -160,7 +159,7 @@ describe('', () => { }) expect(mockNavigate).toHaveBeenCalledWith({ - pathname: '/student/kurser/sokkurs-beta/resultat', + pathname: '/student/kurser/sokkurs/resultat', search: `?${searchParams}`, }) }) diff --git a/public/js/app/pages/NewSearchLandingPage.tsx b/public/js/app/pages/SearchLandingPage.tsx similarity index 91% rename from public/js/app/pages/NewSearchLandingPage.tsx rename to public/js/app/pages/SearchLandingPage.tsx index c54b3ac4..6291441a 100644 --- a/public/js/app/pages/NewSearchLandingPage.tsx +++ b/public/js/app/pages/SearchLandingPage.tsx @@ -13,7 +13,7 @@ import i18n from '../../../../i18n' import { CourseSearchParams, SEARCH_MODES, SearchPageProps } from './types/searchPageTypes' import { CollapseDetails } from '@kth/kth-reactstrap/dist/components/utbildningsinfo' import { FooterContent, HelpTexts, Lead } from '../components' -import { getHelpText } from '../util/newSearchHelper' +import { getHelpText } from '../util/searchHelper' import { useLangHrefUpdate } from '../hooks/useLangHrefUpdate' import { SearchFilters } from '../components' import { stringifyUrlParams } from '../../../../domain/searchParams' @@ -21,7 +21,7 @@ import { pageLink } from '../util/links' const paramsReducer = (state: CourseSearchParams, action: any) => ({ ...state, ...action }) -const NewSearchLandingPage: React.FC = ({ searchMode = SEARCH_MODES.default }) => { +const SearchLandingPage: React.FC = ({ searchMode = SEARCH_MODES.default }) => { const { languageIndex, language } = useStore() const { generalSearch } = i18n.messages[languageIndex] const { searchLabel } = generalSearch @@ -101,7 +101,7 @@ const NewSearchLandingPage: React.FC = ({ searchMode = SEARCH_M ) stringifiedSearchParams = stringifyUrlParams(filteredParams) navigate({ - pathname: '/student/kurser/sokkurs-beta/resultat', + pathname: '/student/kurser/sokkurs/resultat', search: `?${stringifiedSearchParams}`, }) break @@ -111,7 +111,7 @@ const NewSearchLandingPage: React.FC = ({ searchMode = SEARCH_M ) stringifiedSearchParams = stringifyUrlParams(filteredParams) navigate({ - pathname: '/utbildning/forskarutbildning/kurser/sok-beta/resultat', + pathname: '/utbildning/forskarutbildning/kurser/sok/resultat', search: `?${stringifiedSearchParams}`, }) default: @@ -121,7 +121,7 @@ const NewSearchLandingPage: React.FC = ({ searchMode = SEARCH_M return (
- {`${searchHeading} (beta)`} + {searchHeading} = ({ searchMode = SEARCH_M {searchMode === SEARCH_MODES.thirdCycleCourses && ( - + {linkToUsualSearch} )} @@ -143,4 +143,4 @@ const NewSearchLandingPage: React.FC = ({ searchMode = SEARCH_M ) } -export default NewSearchLandingPage +export default SearchLandingPage diff --git a/public/js/app/pages/SearchPage.test.tsx b/public/js/app/pages/SearchPage.test.tsx index b26235b7..2c8ed227 100644 --- a/public/js/app/pages/SearchPage.test.tsx +++ b/public/js/app/pages/SearchPage.test.tsx @@ -1,7 +1,7 @@ import React from 'react' import { render, screen, fireEvent, waitFor, waitForElementToBeRemoved } from '@testing-library/react' import '@testing-library/jest-dom' -import NewSearchPage from './NewSearchPage' +import SearchPage from './SearchPage' import { useStore } from '../mobx' import { courseSearch } from '../util/searchApi' import { TEST_API_ANSWER_RESOLVED } from '../components/mocks/mockKoppsCourseSearch' @@ -42,7 +42,7 @@ jest.mock('../hooks/useLangHrefUpdate', () => ({ const mockDate = new Date('2024-08-19 16:00') -describe('', () => { +describe('', () => { beforeAll(() => { jest.spyOn(global, 'Date').mockImplementation(() => mockDate) ;(useStore as jest.Mock).mockReturnValue({ @@ -72,7 +72,7 @@ describe('', () => { mockSearchParams.append('showOptions', 'onlyEnglish') ;(courseSearch as jest.Mock).mockReturnValue(Promise.resolve(TEST_API_ANSWER_RESOLVED)) - render() + render() expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { pattern: 'Math', @@ -97,7 +97,7 @@ describe('', () => { }) ;(courseSearch as jest.Mock).mockReturnValue(Promise.resolve(TEST_API_ANSWER_RESOLVED)) - render() + render() const searchInput = screen.getByRole('textbox') fireEvent.change(searchInput, { target: { value: 'Physics' } }) @@ -123,7 +123,7 @@ describe('', () => { }), })) - render() + render() const searchInput = screen.getByRole('textbox') expect(searchInput).toBeDisabled() @@ -147,7 +147,7 @@ describe('', () => { test('should update search params and call search API when semesters checkboxes, eduLevel, and showOptions are changed', async () => { ;(courseSearch as jest.Mock).mockReturnValue(Promise.resolve(TEST_API_ANSWER_RESOLVED)) - render() + render() // Verify that the initial API call was made with the correct parameters expect(courseSearch).toHaveBeenCalledWith('en', '/student/kurser', { diff --git a/public/js/app/pages/NewSearchPage.tsx b/public/js/app/pages/SearchPage.tsx similarity index 81% rename from public/js/app/pages/NewSearchPage.tsx rename to public/js/app/pages/SearchPage.tsx index 500a4a7d..bd41c342 100644 --- a/public/js/app/pages/NewSearchPage.tsx +++ b/public/js/app/pages/SearchPage.tsx @@ -15,7 +15,7 @@ import i18n from '../../../../i18n' import { MainContentProps, SEARCH_MODES, SearchPageProps } from './types/searchPageTypes' import { useCourseSearchParams } from '../hooks/useCourseSearchParams' import { STATUS } from '../hooks/types/UseCourseSearchTypes' -import NewSearchResultDisplay from '../components/NewSearchResultDisplay' +import SearchResultDisplay from '../components/SearchResultDisplay' import { CourseSearchResultState } from '../util/types/SearchApiTypes' import { useLangHrefUpdate } from '../hooks/useLangHrefUpdate' import { FILTER_MODES } from '../components/SearchFilters/types' @@ -33,12 +33,12 @@ function _getThisHost(thisHostBaseUrl: string) { return thisHostBaseUrl.slice(-1) === '/' ? thisHostBaseUrl.slice(0, -1) : thisHostBaseUrl } -const NewSearchPage: React.FC = ({ searchMode = SEARCH_MODES.default }) => { +const SearchPage: React.FC = ({ searchMode = SEARCH_MODES.default }) => { const [courseSearchParams, setCourseSearchParams] = useCourseSearchParams() useLangHrefUpdate(courseSearchParams) const { browserConfig, language, languageIndex } = useStore() const { bigSearch, generalSearch, messages } = i18n.messages[languageIndex] - const { main_menu_search_all_new, main_menu_third_cycle_courses_search_new } = messages + const { main_menu_search_all, main_menu_third_cycle_courses_search } = messages const { searchButton } = bigSearch const { resultsHeading, filtersLabel, searchLabel } = generalSearch @@ -59,12 +59,12 @@ const NewSearchPage: React.FC = ({ searchMode = SEARCH_MODES.de switch (searchMode) { case SEARCH_MODES.default: - ancestorItemObj = { href: '/student/kurser/sokkurs-beta', label: main_menu_search_all_new } + ancestorItemObj = { href: '/student/kurser/sokkurs', label: main_menu_search_all } break case SEARCH_MODES.thirdCycleCourses: ancestorItemObj = { - href: '/utbildning/forskarutbildning/kurser/sok-beta', - label: main_menu_third_cycle_courses_search_new, + href: '/utbildning/forskarutbildning/kurser/sok', + label: main_menu_third_cycle_courses_search, } default: break @@ -81,17 +81,17 @@ const NewSearchPage: React.FC = ({ searchMode = SEARCH_MODES.de /> - {`${resultsHeading} (beta)`} + {resultsHeading} - + ) } -export default NewSearchPage +export default SearchPage diff --git a/public/js/app/pages/__snapshots__/CourseSearchLayout.test.js.snap b/public/js/app/pages/__snapshots__/CourseSearchLayout.test.js.snap deleted file mode 100644 index 55f6032e..00000000 --- a/public/js/app/pages/__snapshots__/CourseSearchLayout.test.js.snap +++ /dev/null @@ -1,1405 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Render component CourseSearch within Layout match to snapshot in English 1`] = ` - -
- - -
-
-
-
-

- Search courses -

-
-
-

- Find info on KTH courses: course syllabus, course memo, and course analyses. Search by course name or course code, you can also filter by semester and period. Which courses are included in a program can be found under Programme syllabuses. -

-
- -
-
-
-
-
-
-
-
-
-
-
- - -
-
- - Filter your search choices - -
-
-
-
-
- - Course Start 2021 - -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
-
-
- - Course Start 2022 - -
- - -
-
- - -
-
- - -
-
-
-
-
-
-
-
-
- - Educational level: - -
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
-
-
- - Other options: - -
- - -
-
- - -
-
- - -
-
-
-
-
-
-
-
-
- - School, department, etc - - -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- - Instructions for searching - -
-
-
    -
  • - You can search using part of the course name or code. Searching for "data" will return "Data Storage Paradigms" as well as "Database Techniques" and "Algorithms, Data Structures and Complexity". -
  • -
  • - Searching for more than one word will return courses containing all of the words. Searching for "part 1" will return courses with both part and 1 in course name or code, for example "SI2610 Many Particle Physics" and "DD1343 Computer Science and Numerical Methods, part 1". -
  • -
  • - Both the English and the Swedish course names are searched. Searching for "metod" will return the course "Computer Science Methods" with the Swedish name "Datalogisk metod". -
  • -
  • - At most 250 hits will be displayed. If you get too many hits, try to narrow the search conditions. -
  • -
  • - The search does not distinguish between upper and lower case characters. -
  • -
  • - It is not possible to use special characters, like quotes, in the search. These characters will be removed before the search is performed. -
  • -
  • - Your search may be restricted to courses starting in a specific term. By default, course are found without regard for when the course is offered. -
  • -
  • - Your search may be restricted to courses taught in English. By default, courses are found without regard for tutoring language. -
  • -
  • - You may select to show courses that are no longer offered or dormant at KTH (terminated courses). By default, these are not shown. -
  • -
  • - If you have questions or feedback on the course search, please contact - - kopps@kth.se - - . -
  • -
-
-
-
-
-
-
-
- -
-
-
-
-
-`; - -exports[`Render component CourseSearch within Layout match to snapshot in Swedish 1`] = ` - -
- - -
-
-
-
-

- Sök kurser -

-
-
-

- Här finns info om kurser på KTH: kursplan, kurs-PM och kursanalyser. Sök på kursnamn eller kurskod, du kan även filtrera på termin och läsperiod. Vilka kurser som ingår i ett program finns under Utbildningsplaner. -

-
- -
-
-
-
-
-
-
-
-
-
-
- - -
-
- - Filtrera dina sökval - -
-
-
-
-
- - Kursstart 2021 - -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
-
-
- - Kursstart 2022 - -
- - -
-
- - -
-
- - -
-
-
-
-
-
-
-
-
- - Utbildningsnivå: - -
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
-
-
- - Övrigt: - -
- - -
-
- - -
-
- - -
-
-
-
-
-
-
-
-
- - Skola, avdelning, etc - - -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- - Få hjälp med sökningen - -
-
-
    -
  • - Du kan söka på del av kursnamn eller kurskod. En sökning efter "data" hittar exempelvis såväl "Databasteknik", "Datalogi" som "Algoritmer, datastrukturer och komplexitet". -
  • -
  • - Skriver du flera ord i fältet hittas kurser som innehåller samtliga ord. -
  • -
  • - Du kan även söka på kursens engelska namn. -
  • -
  • - Sökningen visar max 250 träffar. Får du för många träffar, försök att förfina sökvillkoren. -
  • -
  • - Sökningen gör ingen skillnad på versaler och gemener (stora och små bokstäver). -
  • -
  • - Det går inte att använda specialtecken (t.ex. citationstecken). Dessa tas bort innan sökningen utförs. -
  • -
  • - Du kan avgränsa sökningen till att endast visa kurser som startar en specifik termin. Standardinställningen är att hitta kurser oavsett när de går. -
  • -
  • - Du kan avgränsa sökningen till att endast visa kurser med engelska som undervisningsspråk. Standardinställningen är att hitta kurser oavsett undervisningsspråk. -
  • -
  • - Du kan välja att även visa kurser som ej längre ges på KTH. Standardinställning är att dessa inte visas. -
  • -
  • - Om du har synpunkter eller frågor gällande kurssökningen, kontakta - - kopps@kth.se - - . -
  • -
-
-
-
-
-
-
-
- -
-
-
-
-
-`; diff --git a/public/js/app/pages/mocks/Appendix1ApplicationStore.js b/public/js/app/pages/mocks/Appendix1ApplicationStore.js index c2dcc98b..9060cdce 100644 --- a/public/js/app/pages/mocks/Appendix1ApplicationStore.js +++ b/public/js/app/pages/mocks/Appendix1ApplicationStore.js @@ -3,10 +3,8 @@ const applicationStores = [ browserConfig: { proxyPrefixPath: { uri: '/student/kurser', - courseSearch: '/student/kurser/sokkurs', - newSearchPage: '/student/kurser/sokkurs-beta', + searchPage: '/student/kurser/sokkurs', courseSearchInternApi: '/student/kurser/intern-api/sok', - courseSearchInternApiBeta: '/student/kurser/intern-api/sokBeta', department: '/student/kurser/org', programme: '/student/kurser/program', programmesList: '/student/kurser/kurser-inom-program', @@ -521,10 +519,8 @@ const applicationStores = [ browserConfig: { proxyPrefixPath: { uri: '/student/kurser', - courseSearch: '/student/kurser/sokkurs', - newSearchPage: '/student/kurser/sokkurs-beta', + searchPage: '/student/kurser/sokkurs', courseSearchInternApi: '/student/kurser/intern-api/sok', - courseSearchInternApiBeta: '/student/kurser/intern-api/sokBeta', department: '/student/kurser/org', programme: '/student/kurser/program', programmesList: '/student/kurser/kurser-inom-program', @@ -3507,10 +3503,8 @@ const applicationStores = [ browserConfig: { proxyPrefixPath: { uri: '/student/kurser', - courseSearch: '/student/kurser/sokkurs', - newSearchPage: '/student/kurser/sokkurs-beta', + searchPage: '/student/kurser/sokkurs', courseSearchInternApi: '/student/kurser/intern-api/sok', - courseSearchInternApiBeta: '/student/kurser/intern-api/sokBeta', department: '/student/kurser/org', programme: '/student/kurser/program', programmesList: '/student/kurser/kurser-inom-program', diff --git a/public/js/app/stores/createApplicationStore.js b/public/js/app/stores/createApplicationStore.js index eb28cd6e..dbd4b368 100644 --- a/public/js/app/stores/createApplicationStore.js +++ b/public/js/app/stores/createApplicationStore.js @@ -6,7 +6,7 @@ import createCommonStore from './commonStore' import createCurriculumStore from './curriculumStore' import createStudyProgrammeStore from './studyProgrammeStore' import createSearchCoursesStore from './searchCoursesStore' -import { createNewSearchPageStore } from './newSearchPageStore' +import { createSearchPageStore } from './searchPageStore' import createAppendix1Store from './appendix1Store' import createAppendix2Store from './appendix2Store' import createLiteratureStore from './literatureStore' @@ -135,16 +135,11 @@ function createApplicationStore(storeId) { ...createCommonStore(), ...createCurriculumStore(), } - case 'newSearchPage': + case 'SearchPage': return { ...createCommonStore(), ...createStore(), - ...createNewSearchPageStore(), - } - case 'searchCourses': - return { - ...createCommonStore(), - ...createSearchCoursesStore(), + ...createSearchPageStore(), } case 'objectives': case 'extent': diff --git a/public/js/app/stores/newSearchPageStore.ts b/public/js/app/stores/searchPageStore.ts similarity index 91% rename from public/js/app/stores/newSearchPageStore.ts rename to public/js/app/stores/searchPageStore.ts index cf4d0671..8ffa3e06 100644 --- a/public/js/app/stores/newSearchPageStore.ts +++ b/public/js/app/stores/searchPageStore.ts @@ -15,9 +15,8 @@ const setCurrentSchoolsWithDepartments: SetSchoolsWithDepartments = function (cu this.currentSchoolsWithDepartments = currentSchoolsWithDepartments } -export function createNewSearchPageStore(): SearchCoursesStore { +export function createSearchPageStore(): SearchCoursesStore { const searchCoursesStore: SearchCoursesStore = { - isNewSearch: true, schoolsWithDepartments: [], currentSchoolsWithDepartments: [], deprecatedSchoolsWithDepartments: [], diff --git a/public/js/app/stores/types/searchPageStoreTypes.ts b/public/js/app/stores/types/searchPageStoreTypes.ts index cb87adbe..c9c36b5c 100644 --- a/public/js/app/stores/types/searchPageStoreTypes.ts +++ b/public/js/app/stores/types/searchPageStoreTypes.ts @@ -34,7 +34,6 @@ export type SetDepartmentCodeOrPrefix = (departmentCodeOrPrefix: DepartmentCodeO export type ClearStore = () => void export interface SearchCoursesStore { - isNewSearch: Boolean schoolsWithDepartments: SchoolsWithDepartments[] currentSchoolsWithDepartments: SchoolsWithDepartments[] deprecatedSchoolsWithDepartments: SchoolsWithDepartments[] diff --git a/public/js/app/util/internApi.js b/public/js/app/util/internApi.js deleted file mode 100644 index affbeb89..00000000 --- a/public/js/app/util/internApi.js +++ /dev/null @@ -1,24 +0,0 @@ -import axios from 'axios' - -// eslint-disable-next-line consistent-return -async function koppsCourseSearch(language, proxyUrl, params) { - try { - const result = await axios.get(`${proxyUrl}/intern-api/sok/${language}`, { - params, - }) - if (result) { - if (result.status >= 400) { - return 'ERROR-courseSearch-' + result.status - } - const { data } = result - return data - } - } catch (error) { - if (error.response) { - throw new Error('Unexpected error from courseSearch-' + error.message) - } - throw error - } -} - -export default koppsCourseSearch diff --git a/public/js/app/util/searchApi.ts b/public/js/app/util/searchApi.ts index 3b42d5f3..905645db 100644 --- a/public/js/app/util/searchApi.ts +++ b/public/js/app/util/searchApi.ts @@ -11,7 +11,7 @@ export async function courseSearch( try { // Constructing the URL with query parameters const baseUrl = new URL(proxyUrl, window.location.origin).href - const url = new URL(`${baseUrl}/intern-api/sokBeta/${language}`) + const url = new URL(`${baseUrl}/intern-api/sok/${language}`) Object.keys(params).forEach(key => { if (Array.isArray(params[key])) { params[key].forEach((item: any) => url.searchParams.append(`${key}[]`, item)) diff --git a/public/js/app/util/searchHelper.js b/public/js/app/util/searchHelper.js deleted file mode 100644 index 97e91b4d..00000000 --- a/public/js/app/util/searchHelper.js +++ /dev/null @@ -1,31 +0,0 @@ -import i18n from '../../../../i18n' - -function getHelpText(langIndex, nameOfInstruction, instructionKeys) { - /** - * Retrieves a list of translated instructional texts based on the given language index, - * the name of the instruction set, and the specific instruction keys. - */ - - const messages = i18n.messages[langIndex] - const instructions = messages[nameOfInstruction] - // instructions is an object containing all the instructions for the specified language and instruction set. - - const instructionsTexts = instructionKeys.map(s => instructions[s]) - // instructionsTexts is the list of translated instructions based on the provided instructionKeys. - - return instructionsTexts -} - -function hasValue(param) { - if (!param || param === null || param === '') return false - if (typeof param === 'object' && param.length === 0) return false - if (typeof param === 'string' && param.trim().length === 0) return false - return true -} - -function openOptionsInCollapse(hasChosenOptions) { - if (Object.values(hasChosenOptions).length === 0) return false - return true -} - -export { getHelpText, hasValue, openOptionsInCollapse } diff --git a/public/js/app/util/newSearchHelper.ts b/public/js/app/util/searchHelper.ts similarity index 98% rename from public/js/app/util/newSearchHelper.ts rename to public/js/app/util/searchHelper.ts index 79179c93..8822b2a1 100644 --- a/public/js/app/util/newSearchHelper.ts +++ b/public/js/app/util/searchHelper.ts @@ -12,7 +12,7 @@ import { import { Link } from '@kth/kth-reactstrap/dist/components/studinfo' import { courseLink } from './links' import React from 'react' -import { formatShortTerm, formatTermByYearAndPeriod } from '../../../../domain/term' +import { formatTermByYearAndPeriod } from '../../../../domain/term' export const getHelpText: GetHelpText = (langIndex, nameOfInstruction, instructionKeys) => { /** diff --git a/server/controllers/index.js b/server/controllers/index.js index adefa21d..d17faa67 100755 --- a/server/controllers/index.js +++ b/server/controllers/index.js @@ -17,11 +17,10 @@ module.exports = { Programme: require('./programmeCtrl'), ProgrammesList: require('./programmesListCtrl'), SchoolsList: require('./schoolsListCtrl'), - Search: require('./searchCtrl'), StudyHandBook: require('./studyHandBookCtrl'), System: require('./systemCtrl'), ThirdCycleStudySchoolsList: require('./thirdCycleStudySchoolsListCtrl'), ThirdCycleStudyDepartment: require('./thirdCycleStudyDepartmentCtrl'), PDFExport: require('./pdfCtrl'), - NewSearchPage: require('./newSearchPageCtrl'), + SearchPage: require('./searchPageCtrl'), } diff --git a/server/controllers/searchCtrl.js b/server/controllers/searchCtrl.js deleted file mode 100644 index 2bd169b1..00000000 --- a/server/controllers/searchCtrl.js +++ /dev/null @@ -1,167 +0,0 @@ -const log = require('@kth/log') -const language = require('@kth/kth-node-web-common/lib/language') - -const { browser: browserConfig, server: serverConfig } = require('../configuration') -const i18n = require('../../i18n') - -const { createBreadcrumbs, createThirdCycleBreadcrumbs } = require('../utils/breadcrumbUtil') - -const { getServerSideFunctions } = require('../utils/serverSideRendering') - -const koppsApi = require('../kopps/koppsApi') -const { stringifyKoppsSearchParams } = require('../../domain/searchParams') -const { compareSchools, filterOutDeprecatedSchools } = require('../../domain/schools') - -async function searchThirdCycleCourses(req, res, next) { - try { - const lang = language.getLanguage(res) - - let klaroAnalyticsConsentCookie = false - if (req.cookies.klaro) { - const consentCookiesArray = req.cookies.klaro.slice(1, -1).split(',') - // eslint-disable-next-line prefer-destructuring - const analyticsConsentCookieString = consentCookiesArray - .find(cookie => cookie.includes('analytics-consent')) - .split(':')[1] - // eslint-disable-next-line no-const-assign - klaroAnalyticsConsentCookie = analyticsConsentCookieString === 'true' - } - - const { pattern, showOptions, department } = req.query - const { createStore, getCompressedStoreCode, renderStaticPage } = getServerSideFunctions() - - const applicationStore = createStore('searchCourses') - await _fillApplicationStoreWithAllSchools({ applicationStore, lang }) - applicationStore.setLanguage(lang) - applicationStore.setBrowserConfig(browserConfig, serverConfig.hostUrl) - - applicationStore.setPattern(pattern) - applicationStore.setShowOptions(showOptions) - applicationStore.setDepartmentCodeOrPrefix(department) - - const compressedStoreCode = getCompressedStoreCode(applicationStore) - - const { thirdCycleCourseSearch: basename, uri: proxyPrefix } = serverConfig.proxyPrefixPath - const view = renderStaticPage({ applicationStore, location: req.url, basename: basename }) - const title = i18n.message('main_menu_third_cycle_courses_search', lang) - const breadcrumbsList = createThirdCycleBreadcrumbs(lang) - - res.render('app/index', { - html: view, - title, - compressedStoreCode, - description: '', - lang, - proxyPrefix, - toolbarUrl: serverConfig.toolbar.url, - theme: 'external', - klaroAnalyticsConsentCookie, - breadcrumbsList, - }) - } catch (err) { - log.error('Error', { error: err }) - next(err) - } -} - -async function _fillApplicationStoreWithAllSchools({ applicationStore, lang }) { - applicationStore.setLanguage(lang) - applicationStore.setBrowserConfig(browserConfig, serverConfig.hostUrl) - - const listForActiveCourses = true - const params = { - departmentCriteria: koppsApi.DEPARTMENT_CRITERIA.HAS_COURSES, - listForActiveCourses, - lang, - } - const { schoolsWithDepartments } = await koppsApi.listSchoolsWithDepartments(params) - const { currentSchoolsWithDepartments, deprecatedSchoolsWithDepartments } = filterOutDeprecatedSchools( - schoolsWithDepartments, - lang - ) - deprecatedSchoolsWithDepartments.sort(compareSchools) - currentSchoolsWithDepartments.sort(compareSchools) - applicationStore.setCurrentSchoolsWithDepartments(currentSchoolsWithDepartments) - applicationStore.setDeprecatedSchoolsWithDepartments(deprecatedSchoolsWithDepartments) - schoolsWithDepartments.sort(compareSchools) - applicationStore.setSchoolsWithDepartments(schoolsWithDepartments) -} - -async function searchAllCourses(req, res, next) { - try { - const lang = language.getLanguage(res) - - let klaroAnalyticsConsentCookie = false - if (req.cookies.klaro) { - const consentCookiesArray = req.cookies.klaro.slice(1, -1).split(',') - // eslint-disable-next-line prefer-destructuring - const analyticsConsentCookieString = consentCookiesArray - .find(cookie => cookie.includes('analytics-consent')) - .split(':')[1] - // eslint-disable-next-line no-const-assign - klaroAnalyticsConsentCookie = analyticsConsentCookieString === 'true' - } - - const { department, pattern, eduLevel, showOptions, period } = req.query - const { createStore, getCompressedStoreCode, renderStaticPage } = getServerSideFunctions() - - const applicationStore = createStore('searchCourses') - await _fillApplicationStoreWithAllSchools({ applicationStore, lang }) - - applicationStore.setPattern(pattern) - applicationStore.setEduLevels(eduLevel) - applicationStore.setShowOptions(showOptions) - applicationStore.setPeriods(period) - applicationStore.setDepartmentCodeOrPrefix(department) - - const compressedStoreCode = getCompressedStoreCode(applicationStore) - - const { courseSearch: basename, uri: proxyPrefix } = serverConfig.proxyPrefixPath - const view = renderStaticPage({ applicationStore, location: req.url, basename: basename }) - const title = i18n.message('main_menu_search_all', lang) - const breadcrumbsList = createBreadcrumbs(lang) - - res.render('app/index', { - html: view, - title, - compressedStoreCode, - description: '', - lang, - proxyPrefix, - toolbarUrl: serverConfig.toolbar.url, - studentWeb: true, - theme: 'student-web', - klaroAnalyticsConsentCookie, - breadcrumbsList, - }) - } catch (err) { - log.error('Error', { error: err }) - next(err) - } -} - -// eslint-disable-next-line consistent-return -async function performCourseSearch(req, res, next) { - const { lang } = req.params - - const { query } = req - // Example: `text_pattern=${pattern}` - const searchParamsStr = stringifyKoppsSearchParams(query) - - try { - log.debug(` trying to perform a search of courses with ${searchParamsStr} transformed from parameters: `, { query }) - - const apiResponse = await koppsApi.getSearchResults(searchParamsStr, lang) - log.debug(` performCourseSearch with ${searchParamsStr} response: `, apiResponse) - return res.json(apiResponse) - } catch (error) { - log.error(` Exception from performCourseSearch with ${searchParamsStr}`, { error }) - next(error) - } -} - -module.exports = { - performCourseSearch, - searchAllCourses, - searchThirdCycleCourses, -} diff --git a/server/controllers/searchCtrl.test.js b/server/controllers/searchCtrl.test.js deleted file mode 100644 index c9972c08..00000000 --- a/server/controllers/searchCtrl.test.js +++ /dev/null @@ -1,175 +0,0 @@ -const log = require('@kth/log') - -const koppsApi = require('../kopps/koppsApi') -const { TEST_API_ANSWER_ALGEBRA } = require('../mocks/mockKoppsApi') -const { performCourseSearch } = require('./searchCtrl') - -jest.mock('../configuration', () => ({ server: {} })) -jest.mock('../kopps/koppsApi', () => ({ getSearchResults: jest.fn() })) -jest.mock('@kth/log') -log.info = jest.fn() -log.debug = jest.fn() -log.error = jest.fn() - -const { getSearchResults } = koppsApi - -const langSv = 'sv' -const langEn = 'en' - -const mReq = (mockedClientQuery, lang) => ({ - params: { lang }, - query: mockedClientQuery, -}) - -const mRes = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), -} - -const mockNext = () => { - const next = {} - return next -} -describe('Controller searchCtrl, function performCourseSearch', () => { - test('search by pattern in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch(mReq({ pattern: 'Algebra' }, langEn), mRes, mockNext()) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith('text_pattern=Algebra', langEn) - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - - test('search by pattern in swedish', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch(mReq({ pattern: 'Algebra' }, langSv), mRes, mockNext()) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith('text_pattern=Algebra', langSv) - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - - test('search by one educational level param in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch(mReq({ eduLevel: ['0'] }, langEn), mRes, mockNext()) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith('educational_level=PREPARATORY', 'en') - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - test('search by all educational level param in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch(mReq({ eduLevel: ['0', '1', '2', '3'] }, langEn), mRes, mockNext()) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith( - 'educational_level=PREPARATORY&educational_level=BASIC&educational_level=ADVANCED&educational_level=RESEARCH', - 'en' - ) - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - // showOptions - test('search cancelled courses in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch(mReq({ showOptions: ['showCancelled'] }, langEn), mRes, mockNext()) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith('flag=include_non_active', 'en') - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - test('search by all extra options flag level param in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch( - mReq({ showOptions: ['onlyEnglish', 'showCancelled', 'onlyMHU'] }, langEn), - mRes, - mockNext() - ) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith( - 'flag=in_english_only&flag=include_non_active&flag=only_mhu', - 'en' - ) - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - - // period - test('search by third spring period (Spring 2021 period 3) in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch(mReq({ period: ['20211:3'] }, langEn), mRes, mockNext()) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith('term_period=20211%3A3', 'en') - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - - test('search by fourth spring period (Spring 2021 period 4) in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch(mReq({ period: ['20211:4'] }, langEn), mRes, mockNext()) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith('term_period=20211%3A4', 'en') - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - - test('search by summer period 2021 sommar when both spring and autumn periods are presented in a list in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch(mReq({ period: ['2021:summer'] }, langEn), mRes, mockNext()) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith('term_period=20211%3A5&term_period=20212%3A0', 'en') - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - - test('search by first autumn period (Autumn 2021 period 1) in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch(mReq({ period: ['20212:1'] }, langEn), mRes, mockNext()) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith('term_period=20212%3A1', 'en') - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - - test('search by second autumn period (Autumn 2021 period 2) in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch(mReq({ period: ['20212:2'] }, langEn), mRes, mockNext()) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith('term_period=20212%3A2', 'en') - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - - test('search by several periods in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch(mReq({ period: ['2021:summer', '20212:2', '20211:4'] }, langEn), mRes, mockNext()) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith( - 'term_period=20211%3A5&term_period=20212%3A0&term_period=20212%3A2&term_period=20211%3A4', - 'en' - ) - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - - // department - test('search by school/department ABD in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch(mReq({ department: 'ADB' }, langEn), mRes, mockNext()) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith('department_prefix=ADB', 'en') - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) - - // all parameters - test('search by school/department ABD in english', async () => { - getSearchResults.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) - await performCourseSearch( - mReq( - { - eduLevel: ['0', '1', '2', '3'], - department: 'ADB', - period: ['20212:2', '20211:4'], - pattern: 'Algebra', - showOptions: ['onlyEnglish', 'showCancelled', 'onlyMHU'], - }, - langEn - ), - mRes, - mockNext() - ) - - expect(koppsApi.getSearchResults).toHaveBeenCalledWith( - 'educational_level=PREPARATORY&educational_level=BASIC&educational_level=ADVANCED&educational_level=RESEARCH&flag=in_english_only&flag=include_non_active&flag=only_mhu&term_period=20212%3A2&term_period=20211%3A4&text_pattern=Algebra&department_prefix=ADB', - 'en' - ) - expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) - }) -}) diff --git a/server/controllers/newSearchPageCtrl.js b/server/controllers/searchPageCtrl.js similarity index 94% rename from server/controllers/newSearchPageCtrl.js rename to server/controllers/searchPageCtrl.js index b6c52430..f03bfe34 100644 --- a/server/controllers/newSearchPageCtrl.js +++ b/server/controllers/searchPageCtrl.js @@ -10,7 +10,7 @@ const koppsApi = require('../kopps/koppsApi') const { searchCourses } = require('../ladok/ladokApi') const { browser: browserConfig, server: serverConfig } = require('../configuration') -const { createBreadcrumbs } = require('../utils/breadcrumbUtil') +const { createBreadcrumbs, createThirdCycleBreadcrumbs } = require('../utils/breadcrumbUtil') const { getServerSideFunctions } = require('../utils/serverSideRendering') const { compareSchools, filterOutDeprecatedSchools } = require('../../domain/schools') const { stringifyKoppsSearchParams } = require('../../domain/searchParams') @@ -71,8 +71,8 @@ async function renderSearchPage( async function searchAllCourses(req, res, next) { await renderSearchPage(req, res, next, { - storeId: 'newSearchPage', - basenameKey: 'newSearchPage', + storeId: 'SearchPage', + basenameKey: 'searchPage', titleKey: 'main_menu_search_all', breadcrumbsFn: createBreadcrumbs, }) @@ -80,7 +80,7 @@ async function searchAllCourses(req, res, next) { async function searchThirdCycleCourses(req, res, next) { await renderSearchPage(req, res, next, { - storeId: 'newSearchPage', + storeId: 'SearchPage', basenameKey: 'thirdCycleCourseSearch', titleKey: 'main_menu_third_cycle_courses_search', breadcrumbsFn: createThirdCycleBreadcrumbs, @@ -89,7 +89,7 @@ async function searchThirdCycleCourses(req, res, next) { }) } -async function performCourseSearchBeta(req, res, next) { +async function performCourseSearch(req, res, next) { const { lang } = req.params const { query } = req @@ -122,7 +122,7 @@ async function performCourseSearchBeta(req, res, next) { if (level === '1') return '2007GKURS' if (level === '2') return '2007AKURS' if (level === '3') return '2007FKURS' - }) // todo - this can be moved to search params after we decided to use the beta search as the main search + }) // todo - this can be moved to search params after we are done with ladokAPI and we are sure about the values const searchParams = { kodEllerBenamning: query.pattern ? query.pattern : undefined, @@ -171,5 +171,5 @@ async function _fillApplicationStoreWithAllSchools({ applicationStore, lang }) { module.exports = { searchAllCourses, searchThirdCycleCourses, - performCourseSearchBeta, + performCourseSearch, } diff --git a/server/controllers/searchPageCtrl.test.js b/server/controllers/searchPageCtrl.test.js new file mode 100644 index 00000000..447ace04 --- /dev/null +++ b/server/controllers/searchPageCtrl.test.js @@ -0,0 +1,192 @@ +const log = require('@kth/log') + +const koppsApi = require('../kopps/koppsApi') +const ladokApi = require('../ladok/ladokApi') +const { TEST_API_ANSWER_ALGEBRA } = require('../mocks/mockLadokApi') +const { performCourseSearch } = require('./searchPageCtrl') + +jest.mock('../configuration', () => ({ server: {} })) +jest.mock('../ladok/ladokApi', () => ({ searchCourses: jest.fn() })) +jest.mock('../kopps/koppsApi', () => ({ searchCourses: jest.fn() })) +jest.mock('@kth/log') +log.info = jest.fn() +log.debug = jest.fn() +log.error = jest.fn() + +const { searchCourses } = ladokApi + +const langSv = 'sv' +const langEn = 'en' + +const mReq = (mockedClientQuery, lang) => ({ + params: { lang }, + query: mockedClientQuery, +}) + +const mRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), +} + +const mockNext = () => { + const next = {} + return next +} +describe('Controller searchCtrl, function performCourseSearch', () => { + test('search by pattern in english', async () => { + searchCourses.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) + await performCourseSearch(mReq({ pattern: 'Algebra' }, langEn), mRes, mockNext()) + + expect(ladokApi.searchCourses).toHaveBeenCalledWith( + { + kodEllerBenamning: 'Algebra', + avvecklad: undefined, + organisation: undefined, + sprak: undefined, + startPeriod: undefined, + utbildningsniva: undefined, + }, + langEn + ) + expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) + }) + + test('search by pattern in swedish', async () => { + searchCourses.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) + await performCourseSearch(mReq({ pattern: 'Algebra' }, langSv), mRes, mockNext()) + + expect(ladokApi.searchCourses).toHaveBeenCalledWith( + { + kodEllerBenamning: 'Algebra', + avvecklad: undefined, + organisation: undefined, + sprak: undefined, + startPeriod: undefined, + utbildningsniva: undefined, + }, + langSv + ) + expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) + }) + + test('search by one educational level param in english', async () => { + searchCourses.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) + await performCourseSearch(mReq({ eduLevel: ['0'] }, langEn), mRes, mockNext()) + + expect(ladokApi.searchCourses).toHaveBeenCalledWith( + { + kodEllerBenamning: undefined, + avvecklad: undefined, + organisation: undefined, + sprak: undefined, + startPeriod: undefined, + utbildningsniva: ['FUPKURS'], + }, + 'en' + ) + expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) + }) + test('search by all educational level param in english', async () => { + searchCourses.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) + await performCourseSearch(mReq({ eduLevel: ['0', '1', '2', '3'] }, langEn), mRes, mockNext()) + + expect(ladokApi.searchCourses).toHaveBeenCalledWith( + { + kodEllerBenamning: undefined, + avvecklad: undefined, + organisation: undefined, + sprak: undefined, + startPeriod: undefined, + utbildningsniva: ['FUPKURS', '2007GKURS', '2007AKURS', '2007FKURS'], + }, + 'en' + ) + expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) + }) + // showOptions + test('search cancelled courses in english', async () => { + searchCourses.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) + await performCourseSearch(mReq({ showOptions: ['showCancelled'] }, langEn), mRes, mockNext()) + + expect(ladokApi.searchCourses).toHaveBeenCalledWith( + { + kodEllerBenamning: undefined, + avvecklad: 'true', + organisation: undefined, + sprak: undefined, + startPeriod: undefined, + utbildningsniva: undefined, + }, + 'en' + ) + expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) + }) + test('search by all extra options flag level param in english', async () => { + searchCourses.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) + await performCourseSearch(mReq({ showOptions: ['onlyEnglish', 'showCancelled'] }, langEn), mRes, mockNext()) + + expect(ladokApi.searchCourses).toHaveBeenCalledWith( + { + kodEllerBenamning: undefined, + avvecklad: 'true', + organisation: undefined, + sprak: 'ENG', + startPeriod: undefined, + utbildningsniva: undefined, + }, + 'en' + ) + expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) + }) + + // department + test('search by school/department ABD in english', async () => { + searchCourses.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) + await performCourseSearch(mReq({ department: 'ADB' }, langEn), mRes, mockNext()) + + expect(ladokApi.searchCourses).toHaveBeenCalledWith( + { + kodEllerBenamning: undefined, + avvecklad: undefined, + organisation: 'ADB', + sprak: undefined, + startPeriod: undefined, + utbildningsniva: undefined, + }, + 'en' + ) + expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) + }) + + // all parameters + test('search all parameters in english', async () => { + searchCourses.mockReturnValue(Promise.resolve(TEST_API_ANSWER_ALGEBRA)) + await performCourseSearch( + mReq( + { + eduLevel: ['0', '1', '2', '3'], + department: 'ADB', + semesters: ['HT2021', 'VT2021'], + pattern: 'Algebra', + showOptions: ['onlyEnglish', 'showCancelled', 'onlyMHU'], + }, + langEn + ), + mRes, + mockNext() + ) + + expect(ladokApi.searchCourses).toHaveBeenCalledWith( + { + kodEllerBenamning: 'Algebra', + avvecklad: 'true', + organisation: 'ADB', + sprak: 'ENG', + startPeriod: ['HT2021', 'VT2021'], + utbildningsniva: ['FUPKURS', '2007GKURS', '2007AKURS', '2007FKURS'], + }, + 'en' + ) + expect(mRes.status().json).toBeCalledWith(TEST_API_ANSWER_ALGEBRA) + }) +}) diff --git a/server/kopps/koppsApi.js b/server/kopps/koppsApi.js index 6bc21bb4..13c6f303 100644 --- a/server/kopps/koppsApi.js +++ b/server/kopps/koppsApi.js @@ -144,19 +144,6 @@ const getProgramme = async (programmeCode, lang) => { } } -const getSearchResults = async (searchParamsStr, lang) => { - const { client } = koppsApi.koppsApi - const uri = `${slashEndedKoppsBase}courses/search?${searchParamsStr}&l=${lang}` - - try { - const response = await client.getAsync({ uri, useCache: true }) - return response.body - } catch (error) { - log.error('Exception calling KOPPS API in koppsApi.getSearchResults', { error }) - throw error - } -} - const getStudyProgrammeVersion = async (programmeCode, validFromTerm, lang) => { const { client } = koppsApi.koppsApi const programmeCodeUpperCase = programmeCode?.toUpperCase() @@ -231,7 +218,6 @@ module.exports = { listSchoolsWithDepartments, getCourses, getProgramme, - getSearchResults, getStudyProgrammeVersion, listCurriculums, listCourseRoundsInYearPlan, diff --git a/server/mocks/mockLadokApi.js b/server/mocks/mockLadokApi.js new file mode 100644 index 00000000..8157535a --- /dev/null +++ b/server/mocks/mockLadokApi.js @@ -0,0 +1,297 @@ +const TEST_API_ANSWER_OVERFLOW = { + searchHits: [], + errorCode: 'search-error-overflow', + errorMessage: 'search-error-overflow', +} + +const TEST_API_ANSWER_NO_HITS = { + searchHits: [], +} + +const TEST_API_ANSWER_UNKNOWN_ERROR = { + searchHits: [], + errorCode: 'search-error-unknown', + errorMessage: 'search-error-unknown', +} + +const TEST_API_ANSWER_RESOLVED = { + searchHits: [ + { + kod: 'AF2402', + benamning: 'Acoustics and Fire', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Engineering', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Basic level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'BASIC', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2022-01-17', + period: 3, + year: 2022, + week: 3, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'AH2905', + benamning: 'Advanced Pavement Engineering Analysis and Design', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Engineering', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, First-cycle', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'First cycle', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'EN', + name: 'English', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + startperiod: [ + { + code: '20212', + inDigits: '20212', + }, + ], + period: [ + { + startperiod: { + code: '20212', + inDigits: '20212', + }, + forstaUndervisningsdatum: { + date: '2021-08-30', + period: 1, + year: 2021, + week: 35, + }, + sistaUndervisningsdatum: { + date: '2021-12-17', + period: 1, + year: 2021, + week: 50, + }, + tillfallesperioderNummer: 1, + }, + ], + schoolCode: 'SCI', + }, + ], +} + +const TEST_API_ANSWER_EMPTY_PARAMS = 'No query restriction was specified' +const TEST_API_ANSWER_ALGEBRA = { + searchHits: [ + { + kod: 'IX1303', + benamning: 'Algebra och geometri', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Mathematics', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Basic level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'BASIC', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + schoolCode: 'SCI', + }, + { + kod: 'SF1624', + benamning: 'Algebra och geometri', + omfattning: { + formattedWithUnit: '7.5 hp', + }, + organisation: { + id: '', + code: 'SCI', + name: 'SCI/Mathematics', + organisationTypeName: 'Department', + }, + studieort: [ + { + id: '', + code: 'MAIN_CAMPUS', + name: 'Main Campus', + }, + ], + utbildningstyp: [ + { + id: '', + code: '', + name: 'Course, Basic level', + creditsUnit: { + code: 'HP', + sv: 'Högskolepoäng', + en: 'Credits', + }, + level: { + code: '1', + name: 'BASIC', + }, + }, + ], + undervisningssprak: [ + { + id: '', + code: 'SV', + name: 'Swedish', + }, + ], + studietakt: [ + { + id: '', + code: '33', + name: 'One-third-time', + takt: 33, + }, + ], + schoolCode: 'SCI', + }, + ], +} + +export { + TEST_API_ANSWER_EMPTY_PARAMS, + TEST_API_ANSWER_UNKNOWN_ERROR, + TEST_API_ANSWER_OVERFLOW, + TEST_API_ANSWER_NO_HITS, + TEST_API_ANSWER_RESOLVED, + TEST_API_ANSWER_ALGEBRA, +} diff --git a/server/server.js b/server/server.js index 4d82aa55..83e17592 100644 --- a/server/server.js +++ b/server/server.js @@ -232,7 +232,7 @@ const { Appendix2, LiteratureList, PDFExport, - NewSearchPage, + SearchPage, } = require('./controllers') const { parseTerm } = require('../domain/term') @@ -252,27 +252,20 @@ appRoute.get( proxyPrefixPath.thirdCycleCoursesPerDepartment + '/:departmentCode', ThirdCycleStudyDepartment.getCoursesPerDepartment ) -appRoute.get('public.searchAllCourses', proxyPrefixPath.courseSearch, Search.searchAllCourses) -appRoute.get('public.searchThirdCycleCourses', proxyPrefixPath.thirdCycleCourseSearch, Search.searchThirdCycleCourses) -appRoute.get('public.newSearchAllCourses', proxyPrefixPath.newSearchPage, NewSearchPage.searchAllCourses) -appRoute.get('public.newSearchAllCoursesResult', proxyPrefixPath.searchResult, NewSearchPage.searchAllCourses) +appRoute.get('public.searchAllCourses', proxyPrefixPath.searchPage, SearchPage.searchAllCourses) +appRoute.get('public.searchAllCoursesResult', proxyPrefixPath.searchResult, SearchPage.searchAllCourses) appRoute.get( - 'public.NewSearchThirdCycleCourses', - proxyPrefixPath.thirdCycleCourseSearchNew, - Search.searchThirdCycleCourses + 'public.SearchThirdCycleCourses', + proxyPrefixPath.thirdCycleCourseSearch, + SearchPage.searchThirdCycleCourses ) appRoute.get( - 'public.NewSearchThirdCycleCoursesResult', - proxyPrefixPath.thirdCycleCourseSearchResultNew, - Search.searchThirdCycleCourses + 'public.SearchThirdCycleCoursesResult', + proxyPrefixPath.thirdCycleCourseSearchResult, + SearchPage.searchThirdCycleCourses ) -appRoute.get('api.searchCourses', proxyPrefixPath.courseSearchInternApi + '/:lang', Search.performCourseSearch) -appRoute.get( - 'api.searchCoursesBeta', - proxyPrefixPath.courseSearchInternApiBeta + '/:lang', - NewSearchPage.performCourseSearchBeta -) +appRoute.get('api.searchCourses', proxyPrefixPath.courseSearchInternApi + '/:lang', SearchPage.performCourseSearch) appRoute.post('api.programmeSyllabusPDF', proxyPrefixPath.programmeSyllabusPDF, PDFExport.performPDFRenderFunction) appRoute.get('redirect.departmentsListThirdCycleStudy', redirectProxyPath.thirdCycleRoot, (req, res) => { From 80b58986c2849d852705dbfea15a1db1b3f95e69 Mon Sep 17 00:00:00 2001 From: amirhossein-haerian Date: Tue, 18 Mar 2025 14:05:50 +0100 Subject: [PATCH 3/5] feat: add the ability to show the converted markdown kurslist on web --- package-lock.json | 7 ++++--- public/js/app/pages/Curriculum.jsx | 8 ++++++-- server/controllers/curriculumCtrl.js | 10 ++++++++++ server/ladok/ladokApi.js | 7 +++++++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index cec17827..e9d2f994 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,10 +94,11 @@ } }, "../studadm-om-kursen-packages/packages/om-kursen-ladok-client": { - "version": "0.0.1", + "name": "@kth/om-kursen-ladok-client", + "version": "1.1.0", "dependencies": { - "ladok-attributvarde-utils": "file:../ladok-attributvarde-utils", - "ladok-mellanlager-client": "file:../ladok-mellanlager-client" + "@kth/ladok-attributvarde-utils": "file:../ladok-attributvarde-utils", + "@kth/ladok-mellanlager-client": "file:../ladok-mellanlager-client" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/public/js/app/pages/Curriculum.jsx b/public/js/app/pages/Curriculum.jsx index 449fba41..c3ce25c6 100644 --- a/public/js/app/pages/Curriculum.jsx +++ b/public/js/app/pages/Curriculum.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/no-danger */ -import React, { Fragment } from 'react' +import React, { Fragment, useEffect, useState } from 'react' import PropTypes from 'prop-types' import { Col, Row } from 'reactstrap' import { CollapseDetails } from '@kth/kth-reactstrap/dist/components/utbildningsinfo' @@ -203,7 +203,7 @@ function CurriculumInfo() { } function ArticleContent() { - const { language, owningSchoolCode, isMissingAdmission, studyYear, term } = useStore() + const { language, owningSchoolCode, isMissingAdmission, studyYear, term, curriculumInfos } = useStore() const t = translate(language) const calculatedStartTerm = calculateStartTerm(term, studyYear) const formattedAcademicYear = formatAcademicYear(calculatedStartTerm) @@ -211,6 +211,10 @@ function ArticleContent() {

{t('curriculums_missing_admission_text')(owningSchoolCode)}

+ ) : Number(term.substring(0, 4)) < 2024 ? ( +
+
+
) : (

{t('curriculums_studyyear_explanation_1')(studyYear)}

diff --git a/server/controllers/curriculumCtrl.js b/server/controllers/curriculumCtrl.js index 2e6b3763..41789f1f 100644 --- a/server/controllers/curriculumCtrl.js +++ b/server/controllers/curriculumCtrl.js @@ -17,6 +17,7 @@ const { fetchAndFillProgrammeDetails, fetchAndFillStudyProgrammeVersion, } = require('../stores/programmeStoreSSR') +const { getSyllabus } = require('../ladok/ladokApi') /** * @@ -81,6 +82,15 @@ function _compareCurriculum(a, b) { */ async function _fetchAndFillCurriculumByStudyYear(options, storeId) { const { applicationStore, lang, programmeCode, studyYear, term } = options + if (Number(term.substring(0, 4)) < 2024) { + try { + const syllabus = await getSyllabus(programmeCode, term, lang) + applicationStore.setCurriculumInfos(syllabus) + return + } catch (error) { + _setErrorMissingAdmission(applicationStore, 404) + } + } const { studyProgrammeId, statusCode } = await fetchAndFillStudyProgrammeVersion({ ...options, storeId }) if (!studyProgrammeId) { _setErrorMissingAdmission(applicationStore, statusCode) diff --git a/server/ladok/ladokApi.js b/server/ladok/ladokApi.js index 81d82aab..8eea7afc 100644 --- a/server/ladok/ladokApi.js +++ b/server/ladok/ladokApi.js @@ -9,6 +9,13 @@ async function searchCourses(pattern, lang) { return courses } +async function getSyllabus(courseCode, semester, lang) { + const client = createApiClient(serverConfig.ladokMellanlagerApi) + const syllabus = await client.getProgramSyllabus(courseCode, semester, lang) + return syllabus +} + module.exports = { searchCourses, + getSyllabus, } From a368b8e67bc0c598bdfab83b6d19bbe3cc31f1f7 Mon Sep 17 00:00:00 2001 From: amirhossein-haerian Date: Tue, 18 Mar 2025 14:10:58 +0100 Subject: [PATCH 4/5] fix: remove to extra imports --- public/js/app/pages/Curriculum.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/app/pages/Curriculum.jsx b/public/js/app/pages/Curriculum.jsx index c3ce25c6..3be173ed 100644 --- a/public/js/app/pages/Curriculum.jsx +++ b/public/js/app/pages/Curriculum.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/no-danger */ -import React, { Fragment, useEffect, useState } from 'react' +import React, { Fragment } from 'react' import PropTypes from 'prop-types' import { Col, Row } from 'reactstrap' import { CollapseDetails } from '@kth/kth-reactstrap/dist/components/utbildningsinfo' From b45a8dbe873cabb180e7556762ea272ed7db9d0d Mon Sep 17 00:00:00 2001 From: amirhossein-haerian Date: Tue, 18 Mar 2025 14:56:27 +0100 Subject: [PATCH 5/5] fix: fix the package naming --- package-lock.json | 10 +++++----- package.json | 2 +- server/ladok/ladokApi.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index e9d2f994..38e0516d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@kth/kth-reactstrap": "^0.4.78", "@kth/log": "^4.0.7", "@kth/monitor": "^4.2.1", + "@kth/om-kursen-ladok-client": "file:../studadm-om-kursen-packages/packages/om-kursen-ladok-client", "@kth/server": "^4.1.0", "@kth/session": "^3.0.9", "@kth/style": "^1.6.0", @@ -36,7 +37,6 @@ "kth-style": "^10.3.0", "mobx": "^6.12.0", "mobx-react": "^9.1.0", - "om-kursen-ladok-client": "file:../studadm-om-kursen-packages/packages/om-kursen-ladok-client", "prop-types": "^15.8.1", "querystring": "^0.2.1", "react": "^18.2.0", @@ -3382,6 +3382,10 @@ "@kth/log": "^4.0.7" } }, + "node_modules/@kth/om-kursen-ladok-client": { + "resolved": "../studadm-om-kursen-packages/packages/om-kursen-ladok-client", + "link": true + }, "node_modules/@kth/server": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@kth/server/-/server-4.1.0.tgz", @@ -14028,10 +14032,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/om-kursen-ladok-client": { - "resolved": "../studadm-om-kursen-packages/packages/om-kursen-ladok-client", - "link": true - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", diff --git a/package.json b/package.json index 04ca4e98..b6dd50a7 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "kth-style": "^10.3.0", "mobx": "^6.12.0", "mobx-react": "^9.1.0", - "om-kursen-ladok-client": "file:../studadm-om-kursen-packages/packages/om-kursen-ladok-client", + "@kth/om-kursen-ladok-client": "file:../studadm-om-kursen-packages/packages/om-kursen-ladok-client", "prop-types": "^15.8.1", "querystring": "^0.2.1", "react": "^18.2.0", diff --git a/server/ladok/ladokApi.js b/server/ladok/ladokApi.js index 8eea7afc..1df42c79 100644 --- a/server/ladok/ladokApi.js +++ b/server/ladok/ladokApi.js @@ -1,6 +1,6 @@ 'use strict' -const { createApiClient } = require('om-kursen-ladok-client') +const { createApiClient } = require('@kth/om-kursen-ladok-client') const serverConfig = require('../configuration').server async function searchCourses(pattern, lang) {