From b44aa9e2363cc9c00b55c3ca7564ee91aba852aa Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:03:41 +0300 Subject: [PATCH 01/36] Suodatinten tila query-parametreihin ja tyylien parantelua --- src/app/components/haku-filters.tsx | 170 ++++++++++++-------- src/app/components/table/list-table.tsx | 2 +- src/app/{lib => }/hooks/useAsiointiKieli.ts | 4 +- src/app/hooks/useDebounce.ts | 29 ++++ src/app/hooks/useQueryParams.ts | 56 +++++++ src/app/lib/kouta-types.ts | 7 +- src/app/theme.tsx | 106 +++++++++++- src/app/wrapper.tsx | 2 +- 8 files changed, 303 insertions(+), 73 deletions(-) rename src/app/{lib => }/hooks/useAsiointiKieli.ts (75%) create mode 100644 src/app/hooks/useDebounce.ts create mode 100644 src/app/hooks/useQueryParams.ts diff --git a/src/app/components/haku-filters.tsx b/src/app/components/haku-filters.tsx index af7f9ade..902bb12c 100644 --- a/src/app/components/haku-filters.tsx +++ b/src/app/components/haku-filters.tsx @@ -1,15 +1,16 @@ 'use client'; -import { ChangeEvent, useState } from 'react'; +import React, { ChangeEvent, useMemo } from 'react'; import { styled, FormControl, - Button, Select, MenuItem, SelectChangeEvent, FormLabel, - Input, + OutlinedInput, + Checkbox, + FormControlLabel, } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; @@ -24,15 +25,18 @@ import { HakuList } from './haku-table'; import { getTranslation } from '../lib/common'; import { useSuspenseQuery } from '@tanstack/react-query'; import { getHaut } from '../lib/kouta'; +import useQueryParams from '@/app/hooks/useQueryParams'; +import { useDebounce } from '../hooks/useDebounce'; const alkamisKausiMatchesSelected = ( haku: Haku, - selectedAlkamisKausi: HaunAlkaminen, + selectedAlkamisKausi?: HaunAlkaminen, ): boolean => - haku.alkamisVuosi === selectedAlkamisKausi.alkamisVuosi && - haku.alkamisKausiKoodiUri.startsWith( - selectedAlkamisKausi.alkamisKausiKoodiUri, - ); + !selectedAlkamisKausi || + (haku.alkamisVuosi === selectedAlkamisKausi.alkamisVuosi && + haku.alkamisKausiKoodiUri.startsWith( + selectedAlkamisKausi.alkamisKausiKoodiUri, + )); const StyledGridContainer = styled(Grid)({ justifyContent: 'space-between', @@ -66,6 +70,22 @@ export const HakuFilters = ({ hakutavat }: { hakutavat: Array }) => { return ; }; +const useQueryParamState = (name: string, emptyValue?: T) => { + const defaultValue = emptyValue ?? ''; + const { queryParams, setQueryParam, removeQueryParam } = useQueryParams(); + const value = queryParams.get(name) ?? defaultValue; + + const setValue = (value?: string) => { + if (!value || value === defaultValue) { + removeQueryParam(name); + } else { + setQueryParam(name, value); + } + }; + + return [value, setValue] as const; +}; + const HakuFiltersInternal = ({ haut, hakutavat, @@ -73,60 +93,67 @@ const HakuFiltersInternal = ({ haut: Haku[]; hakutavat: Koodi[]; }) => { - const [results, setResults] = useState( - haut.filter((h) => h.tila === Tila.JULKAISTU), + const [search, setSearch] = useQueryParamState('search', ''); + + const setSearchDebounce = useDebounce(setSearch, 500); + + const [myosArkistoidut, setMyosArkistoidut] = useQueryParamState( + 'arkistoidut', + 'false', + ); + + const [selectedHakutapa, setSelectedHakutapa] = useQueryParamState( + 'hakutapa', + '', + ); + + const [selectedAlkamisKausi, setSelectedAlkamisKausi] = useQueryParamState( + 'alkamiskausi', + '', ); - const [search, setSearch] = useState(''); - const [selectedTila, setSelectedTila] = useState(Tila.JULKAISTU); - const [selectedAlkamisKausi, setSelectedAlkamisKausi] = - useState(); - const [selectedHakutapa, setSelectedHakutapa] = useState(); - - const alkamisKaudet = getHakuAlkamisKaudet(); - - const filterHaut = ( - search: string, - tila: Tila, - kausi: HaunAlkaminen | undefined, - tapa: Koodi | undefined, - ) => { - const filteredValue = haut.filter( + + const alkamiskaudet = useMemo(getHakuAlkamisKaudet, []); + + const results = useMemo(() => { + const tilat = + myosArkistoidut === 'true' + ? [Tila.JULKAISTU, Tila.ARKISTOITU] + : [Tila.JULKAISTU]; + return haut.filter( (haku: Haku) => - haku.tila == tila && + tilat.includes(haku.tila) && getTranslation(haku.nimi).toLowerCase().includes(search) && - (!kausi || alkamisKausiMatchesSelected(haku, kausi)) && - (!tapa || haku.hakutapaKoodiUri.startsWith(tapa.koodiUri)), + alkamisKausiMatchesSelected( + haku, + alkamiskaudet.find((k) => k.value === selectedAlkamisKausi), + ) && + haku.hakutapaKoodiUri.startsWith(selectedHakutapa), ); - setResults(filteredValue); - }; + }, [ + haut, + search, + myosArkistoidut, + selectedAlkamisKausi, + selectedHakutapa, + alkamiskaudet, + ]); const handleSearchChange = (e: ChangeEvent) => { - const { target } = e; - - const searchStr = target.value.trim().toLowerCase(); - setSearch(searchStr); - filterHaut(searchStr, selectedTila, selectedAlkamisKausi, selectedHakutapa); + const searchStr = e.target.value.trim().toLowerCase(); + setSearchDebounce(searchStr); }; - const toggleSearchActive = () => { - const toggledTila = - selectedTila == Tila.ARKISTOITU ? Tila.JULKAISTU : Tila.ARKISTOITU; - setSelectedTila(toggledTila); - filterHaut(search, toggledTila, selectedAlkamisKausi, selectedHakutapa); + const toggleMyosArkistoidut = (_e: unknown, checked: boolean) => { + setMyosArkistoidut(checked ? 'true' : 'false'); }; const changeHakutapa = (e: SelectChangeEvent) => { - const idx = parseInt(e.target.value); - const tapa = idx > -1 ? hakutavat[idx] : undefined; - setSelectedHakutapa(tapa); - filterHaut(search, selectedTila, selectedAlkamisKausi, tapa); + const tapaKoodiUri = e.target.value; + setSelectedHakutapa(tapaKoodiUri); }; const changeAlkamisKausi = (e: SelectChangeEvent) => { - const idx = parseInt(e.target.value); - const kausi = idx > -1 ? alkamisKaudet[idx] : undefined; - setSelectedAlkamisKausi(kausi); - filterHaut(search, selectedTila, kausi, selectedHakutapa); + setSelectedAlkamisKausi(e.target.value); }; return ( @@ -138,17 +165,27 @@ const HakuFiltersInternal = ({ sx={{ m: 1, minWidth: 180, textAlign: 'left' }} > Hae hakuja - - + + } + /> @@ -161,13 +198,14 @@ const HakuFiltersInternal = ({ data-testid="haku-hakutapa-select" labelId="hakutapa-select-label" name="hakutapa-select" + value={selectedHakutapa} onChange={changeHakutapa} - defaultValue="-1" + displayEmpty={true} > - Valitse... - {hakutavat.map((tapa, index) => { + Valitse... + {hakutavat.map((tapa) => { return ( - + {tapa.nimi.fi} ); //TODO: translate @@ -187,17 +225,15 @@ const HakuFiltersInternal = ({ data-testid="haku-kausi-select" labelId="alkamiskausi-select-label" name="alkamiskausi-select" + displayEmpty={true} onChange={changeAlkamisKausi} - defaultValue="-1" + defaultValue="" > - Valitse... - {alkamisKaudet.map((kausi, index) => { + Valitse... + {alkamiskaudet.map((kausi) => { return ( - - {kausi.alkamisVuosi} {kausi.alkamisKausiNimi} + + {kausi.alkamisKausiNimi} {kausi.alkamisVuosi} ); //TODO: translate })} @@ -205,7 +241,11 @@ const HakuFiltersInternal = ({ - {results && } + {results && results.length === 0 ? ( +

Ei hakutuloksia

+ ) : ( + + )} ); }; diff --git a/src/app/components/table/list-table.tsx b/src/app/components/table/list-table.tsx index c37dfabf..e8f3c6d2 100644 --- a/src/app/components/table/list-table.tsx +++ b/src/app/components/table/list-table.tsx @@ -56,7 +56,7 @@ export const makeCountColumn = ({ export const makeTilaColumn = (): Column => ({ title: 'Tila', key: 'tila', - render: (haku) => {Tila[haku.tila]}, + render: (haku) => {haku.tila}, style: { width: 0, }, diff --git a/src/app/lib/hooks/useAsiointiKieli.ts b/src/app/hooks/useAsiointiKieli.ts similarity index 75% rename from src/app/lib/hooks/useAsiointiKieli.ts rename to src/app/hooks/useAsiointiKieli.ts index 84465a47..99b2953a 100644 --- a/src/app/lib/hooks/useAsiointiKieli.ts +++ b/src/app/hooks/useAsiointiKieli.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; -import { client } from '../http-client'; -import { configuration } from '../configuration'; +import { client } from '../lib/http-client'; +import { configuration } from '../lib/configuration'; export const getAsiointiKieli = () => { return client.get(configuration.asiointiKieliUrl); diff --git a/src/app/hooks/useDebounce.ts b/src/app/hooks/useDebounce.ts new file mode 100644 index 00000000..742b5f09 --- /dev/null +++ b/src/app/hooks/useDebounce.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import React from 'react'; + +export const debounce = any>( + callback: T, + waitFor: number, +) => { + let timeout = 0; + return (...args: Parameters): ReturnType => { + let result: any; + clearTimeout(timeout); + timeout = window.setTimeout(() => { + result = callback(...args); + }, waitFor); + return result; + }; +}; + +export function useDebounce(callback: any, delay: number) { + const callbackRef = React.useRef(callback); + React.useLayoutEffect(() => { + callbackRef.current = callback; + }); + return React.useMemo( + () => debounce((...args) => callbackRef.current(...args), delay), + [delay], + ); +} diff --git a/src/app/hooks/useQueryParams.ts b/src/app/hooks/useQueryParams.ts new file mode 100644 index 00000000..9ff15b98 --- /dev/null +++ b/src/app/hooks/useQueryParams.ts @@ -0,0 +1,56 @@ +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useCallback } from 'react'; + +export default function useQueryParams() { + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + + type UrlParam = { + name: string; + value: string; + }; + + const createQueryStrings = useCallback( + (newparams: UrlParam[]) => { + const params = new URLSearchParams(searchParams?.toString() ?? ''); + newparams.map((p) => params.set(p.name, p.value)); + + return params.toString(); + }, + [searchParams], + ); + const createQueryString = useCallback( + (name: string, value: string) => { + const params = new URLSearchParams(searchParams); + params.set(name, value); + return params.toString(); + }, + [searchParams], + ); + + const removeQueryString = useCallback( + (name: string) => { + const params = new URLSearchParams(searchParams); + params.delete(name); + return params.toString(); + }, + [searchParams], + ); + + const setQueryParam = (queryName: string, value: string) => { + router.push(`${pathname}?${createQueryString(queryName, value)}`); + }; + + const removeQueryParam = (queryName: string) => { + router.push(`${pathname}?${removeQueryString(queryName)}`); + }; + + return { + queryParams: searchParams, + createQueryString, + createQueryStrings, + removeQueryParam, + setQueryParam, + }; +} diff --git a/src/app/lib/kouta-types.ts b/src/app/lib/kouta-types.ts index 35a42fbb..cf67e489 100644 --- a/src/app/lib/kouta-types.ts +++ b/src/app/lib/kouta-types.ts @@ -6,6 +6,7 @@ export type HaunAlkaminen = { alkamisVuosi: number; alkamisKausiKoodiUri: string; alkamisKausiNimi: string; + value: string; }; //TODO: localization @@ -20,11 +21,13 @@ export const getHakuAlkamisKaudet = (): HaunAlkaminen[] => { alkamisVuosi: i, alkamisKausiKoodiUri: 'kausi_s', alkamisKausiNimi: 'SYKSY', + value: `syksy_${i}`, }); alkamiset.push({ alkamisVuosi: i, alkamisKausiKoodiUri: 'kausi_k', alkamisKausiNimi: 'KEVÄT', + value: `kevat_${i}`, }); } return alkamiset; @@ -50,6 +53,6 @@ export type Hakukohde = { }; export enum Tila { - JULKAISTU, - ARKISTOITU, + JULKAISTU = 'julkaistu', + ARKISTOITU = 'arkistoitu', } diff --git a/src/app/theme.tsx b/src/app/theme.tsx index 40b4460e..422c5c22 100644 --- a/src/app/theme.tsx +++ b/src/app/theme.tsx @@ -1,6 +1,8 @@ 'use client'; +import { ButtonOwnProps } from '@mui/material'; import { createTheme } from '@mui/material/styles'; import NextLink, { LinkProps } from 'next/link'; +import { Open_Sans } from 'next/font/google'; import * as React from 'react'; const LinkBehaviour = React.forwardRef( @@ -9,11 +11,44 @@ const LinkBehaviour = React.forwardRef( }, ); +const colors = { + grey900: '#1D1D1D', + grey700: '#4C4C4C', + grey600: '#5D5D5D', + grey500: '#B2B2B2', + grey400: '#CCCCCC', + grey200: '#E6E6E6', + grey50: '#F5F7F9', + + white: '#FFFFFF', + black: '#000000', + + blue1: '#000066', + blue2: '#0033CC', + blue3: '#0041DC', + cyan1: '#006699', + cyan2: '#66CCCC', + cyan3: '#99FFFF', + lightBlue1: '#82D4FF', + lightBlue2: '#C1EaFF', +}; + +const openSans = Open_Sans({ + weight: ['400', '600', '700'], + subsets: ['latin'], + display: 'swap', +}); + const theme = createTheme({ + typography: { + fontFamily: openSans.style.fontFamily, + }, palette: { primary: { - main: '#0a789c', - contrastText: '#fff', + main: colors.blue2, + light: colors.blue3, + dark: colors.blue1, + contrastText: colors.white, }, }, components: { @@ -22,7 +57,74 @@ const theme = createTheme({ component: LinkBehaviour, }, }, + MuiButton: { + styleOverrides: { + root: { + boxShadow: 'none', + textTransform: 'none', + fontWeight: '600', + }, + contained: ({ ownerState, theme }) => { + return { + borderRadius: '2px', + '&:hover': { + backgroundColor: getColorByName(ownerState.color, theme, 'light'), + }, + '&:active': { + backgroundColor: getColorByName(ownerState.color, theme, 'dark'), + }, + }; + }, + outlined: ({ ownerState, theme }) => { + return { + color: getColorByName(ownerState.color, theme, 'main'), + borderRadius: '2px', + border: `3px solid ${getColorByName(ownerState.color, theme, 'main')}`, + '&:hover': { + borderWidth: '3px', + backgroundColor: colors.white, + color: getColorByName(ownerState.color, theme, 'light'), + borderColor: getColorByName(ownerState.color, theme, 'light'), + }, + '&:active': { + borderWidth: '3px', + backgroundColor: colors.white, + color: getColorByName(ownerState.color, theme, 'dark'), + borderColor: getColorByName(ownerState.color, theme, 'dark'), + }, + }; + }, + text: ({ ownerState, theme }) => { + return { + color: getColorByName(ownerState.color, theme, 'main'), + '&:hover': { + color: getColorByName(ownerState.color, theme, 'light'), + }, + '&:active': { + color: getColorByName(ownerState.color, theme, 'dark'), + }, + }; + }, + }, + }, + MuiOutlinedInput: { + styleOverrides: { + root: { + borderRadius: '2px', + }, + }, + }, }, }); +function getColorByName( + colorName: ButtonOwnProps['color'], + customTheme: typeof theme, + mode: 'main' | 'light' | 'dark', +) { + return colorName === 'inherit' + ? 'inherit' + : customTheme.palette[colorName ?? 'primary'][mode]; +} + export default theme; diff --git a/src/app/wrapper.tsx b/src/app/wrapper.tsx index 18c792fd..375a96b1 100644 --- a/src/app/wrapper.tsx +++ b/src/app/wrapper.tsx @@ -1,6 +1,6 @@ 'use client'; import { FullSpinner } from './components/full-spinner'; -import { useAsiointiKieli } from './lib/hooks/useAsiointiKieli'; +import { useAsiointiKieli } from './hooks/useAsiointiKieli'; export default function Wrapper({ children }: { children: React.ReactNode }) { const { isLoading, isError, error } = useAsiointiKieli(); From e9dcce05cf0dcff82c6595598a18d86e040b2575 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:03:41 +0300 Subject: [PATCH 02/36] =?UTF-8?q?Toteutettu=20sivutus=20ja=20viilattu=20ty?= =?UTF-8?q?ylej=C3=A4=20design=20systemin=20mukaisiksi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/haku-filters.tsx | 35 +++++++++++++++++- src/app/theme.tsx | 55 ++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/app/components/haku-filters.tsx b/src/app/components/haku-filters.tsx index 902bb12c..59a5f9e1 100644 --- a/src/app/components/haku-filters.tsx +++ b/src/app/components/haku-filters.tsx @@ -11,6 +11,7 @@ import { OutlinedInput, Checkbox, FormControlLabel, + Pagination, } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; @@ -112,6 +113,12 @@ const HakuFiltersInternal = ({ '', ); + const [page, setPage] = useQueryParamState('page', '1'); + + const pageNum = parseInt(page, 10); + + const [pageSize] = useQueryParamState('page_size', '50'); + const alkamiskaudet = useMemo(getHakuAlkamisKaudet, []); const results = useMemo(() => { @@ -138,6 +145,15 @@ const HakuFiltersInternal = ({ alkamiskaudet, ]); + const pageSizeNum = parseInt(pageSize, 10); + + const pageResults = useMemo(() => { + const start = pageSizeNum * (pageNum - 1); + return results.slice(start, start + pageSizeNum); + }, [results, pageNum, pageSizeNum]); + + console.log({ pageResults, pageSizeNum, pageNum }); + const handleSearchChange = (e: ChangeEvent) => { const searchStr = e.target.value.trim().toLowerCase(); setSearchDebounce(searchStr); @@ -244,7 +260,24 @@ const HakuFiltersInternal = ({ {results && results.length === 0 ? (

Ei hakutuloksia

) : ( - + <> +

Hakuja: {results.length}

+ { + setPage(value.toString()); + }} + /> + + { + setPage(value.toString()); + }} + /> + )} ); diff --git a/src/app/theme.tsx b/src/app/theme.tsx index 422c5c22..e52620fb 100644 --- a/src/app/theme.tsx +++ b/src/app/theme.tsx @@ -13,12 +13,15 @@ const LinkBehaviour = React.forwardRef( const colors = { grey900: '#1D1D1D', + grey800: '#454545', grey700: '#4C4C4C', grey600: '#5D5D5D', - grey500: '#B2B2B2', - grey400: '#CCCCCC', - grey200: '#E6E6E6', - grey50: '#F5F7F9', + grey500: '#6D6D6D', + grey400: '#B2B2B2', + grey300: '#C8C8C8', + grey200: '#DFDFDF', + grey100: '#EDEDED', + grey50: '#F6F6F6', white: '#FFFFFF', black: '#000000', @@ -107,13 +110,55 @@ const theme = createTheme({ }, }, }, - MuiOutlinedInput: { + MuiFormLabel: { + styleOverrides: { + root: { + color: colors.black, + '&.Mui-focused': { + color: colors.black, + }, + }, + }, + }, + MuiInputLabel: { + styleOverrides: { + root: { + color: colors.black, + '&.Mui-focused': { + color: colors.black, + }, + }, + }, + }, + MuiPagination: { + defaultProps: { + variant: 'text', + shape: 'rounded', + }, + }, + MuiPaginationItem: { styleOverrides: { root: { borderRadius: '2px', }, }, }, + MuiOutlinedInput: { + styleOverrides: { + root: ({ theme }) => { + return { + '.MuiOutlinedInput-notchedOutline': { + borderRadius: '2px', + borderWidth: '1px', + }, + '&:hover .MuiOutlinedInput-notchedOutline': { + borderWidth: '2px', + borderColor: theme.palette.primary.main, + }, + }; + }, + }, + }, }, }); From ab0ef7ad84933dcc72f15930b96b0a16d76ad0d0 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:03:41 +0300 Subject: [PATCH 03/36] =?UTF-8?q?Korjattu=20ja=20paranneltu=20Playwright-t?= =?UTF-8?q?estej=C3=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/haku-filters.tsx | 8 ++-- tests/e2e/haut.spec.ts | 62 +++++++++++++++-------------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/app/components/haku-filters.tsx b/src/app/components/haku-filters.tsx index 59a5f9e1..cfedc008 100644 --- a/src/app/components/haku-filters.tsx +++ b/src/app/components/haku-filters.tsx @@ -182,7 +182,6 @@ const HakuFiltersInternal = ({ > Hae hakuja Hakutapa Valitse... {alkamiskaudet.map((kausi) => { + const vuosiKausi = `${kausi.alkamisVuosi} ${kausi.alkamisKausiNimi}`; return ( - {kausi.alkamisKausiNimi} {kausi.alkamisVuosi} + {vuosiKausi} ); //TODO: translate })} @@ -263,6 +261,7 @@ const HakuFiltersInternal = ({ <>

Hakuja: {results.length}

{ @@ -271,6 +270,7 @@ const HakuFiltersInternal = ({ /> { diff --git a/tests/e2e/haut.spec.ts b/tests/e2e/haut.spec.ts index 446bf781..b117d816 100644 --- a/tests/e2e/haut.spec.ts +++ b/tests/e2e/haut.spec.ts @@ -4,20 +4,24 @@ import { expectPageAccessibilityOk, } from './playwright-utils'; -async function selectHakutapa(page: Page, idx: number, expectedOption: string) { - await page.getByTestId('haku-hakutapa-select').click(); - await page.locator(`#menu-hakutapa-select li:nth-child(${idx})`).click(); - await expect( - page.locator('#mui-component-select-hakutapa-select'), - ).toContainText(expectedOption); +async function selectHakutapa(page: Page, expectedOption: string) { + const combobox = page.getByRole('combobox', { name: 'Hakutapa' }); + await combobox.click(); + const listbox = page.getByRole('listbox', { name: 'Hakutapa' }); + await listbox.getByRole('option', { name: expectedOption }).click(); + await expect(combobox).toContainText(expectedOption); } -async function selectKausi(page: Page, idx: number, expectedOption: string) { - await page.getByTestId('haku-kausi-select').click(); - await page.locator(`#menu-alkamiskausi-select li:nth-child(${idx})`).click(); - await expect( - page.locator('#mui-component-select-alkamiskausi-select'), - ).toContainText(expectedOption); +async function selectKausi(page: Page, expectedOption: string) { + const combobox = page.getByRole('combobox', { + name: 'Koulutuksen alkamiskausi', + }); + await combobox.click(); + const listbox = page.getByRole('listbox', { + name: 'Koulutuksen alkamiskausi', + }); + await listbox.getByRole('option', { name: expectedOption }).click(); + await expect(combobox).toContainText(expectedOption); } test.beforeEach(async ({ page }) => { @@ -33,12 +37,13 @@ test('Haku-page accessibility', async ({ page }) => { await expectPageAccessibilityOk(page); }); +const getMyosArkistoidut = (page: Page) => + page.getByRole('checkbox', { name: 'Myös arkistoidut' }); + test('filters haku by published state', async ({ page }) => { - await expect(page.getByTestId('haku-tila-toggle')).toContainText( - 'Julkaistut', - ); + await expect(getMyosArkistoidut(page)).not.toBeChecked(); await expect(page.locator('tbody tr')).toHaveCount(3); - const hakuInput = await page.locator('input[name=haku-search]'); + const hakuInput = await page.getByRole('textbox', { name: 'Hae hakuja' }); hakuInput.fill('Luk'); await expect(page.locator('tbody tr')).toHaveCount(1); await expect(page.locator('tbody tr')).toContainText( @@ -47,14 +52,13 @@ test('filters haku by published state', async ({ page }) => { }); test('filters haku by archived state', async ({ page }) => { - await page.getByTestId('haku-tila-toggle').click(); - await expect(page.getByTestId('haku-tila-toggle')).toContainText( - 'Arkistoidut', - ); - await expect(page.locator('tbody tr')).toHaveCount(3); - const hakuInput = await page.locator('input[name=haku-search]'); + const myosArkistoidut = getMyosArkistoidut(page); + await myosArkistoidut.click(); + await expect(myosArkistoidut).toBeChecked(); + await expect(page.locator('tbody tr')).toHaveCount(6); + const hakuInput = await page.getByRole('textbox', { name: 'Hae hakuja' }); hakuInput.fill('hak'); - await expect(page.locator('tbody tr')).toHaveCount(3); + await expect(page.locator('tbody tr')).toHaveCount(5); hakuInput.fill('Leppä'); await expect(page.locator('tbody tr')).toHaveCount(1); await expect(page.locator('tbody tr')).toContainText( @@ -63,23 +67,23 @@ test('filters haku by archived state', async ({ page }) => { }); test('filters by hakutapa', async ({ page }) => { - await selectHakutapa(page, 5, 'Erillishaku'); + await selectHakutapa(page, 'Erillishaku'); await expect(page.locator('tbody tr')).toHaveCount(1); - await selectHakutapa(page, 6, 'Jatkuva haku'); + await selectHakutapa(page, 'Jatkuva haku'); await expect(page.locator('tbody tr')).toHaveCount(2); }); test('filters by start period', async ({ page }) => { - await selectKausi(page, 2, '2024 SYKSY'); + await selectKausi(page, '2024 SYKSY'); await expect(page.locator('tbody tr')).toHaveCount(1); - await selectKausi(page, 10, '2020 SYKSY'); + await selectKausi(page, '2020 SYKSY'); await expect(page.locator('tbody tr')).toHaveCount(0); }); test('filters by hakutapa and start period', async ({ page }) => { - await selectHakutapa(page, 6, 'Jatkuva haku'); + await selectHakutapa(page, 'Jatkuva haku'); await expect(page.locator('tbody tr')).toHaveCount(2); - await selectKausi(page, 4, '2023 SYKSY'); + await selectKausi(page, '2023 SYKSY'); await expect(page.locator('tbody tr')).toHaveCount(1); }); From 31a37569f7de6d0f1d51c9dd4fd91a00e019fbbd Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:03:41 +0300 Subject: [PATCH 04/36] =?UTF-8?q?Refaktoroitu=20haun=20sivutus=20ja=20suod?= =?UTF-8?q?atukset=20omaan=20hookkiinsa=20ja=20lis=C3=A4tty=20sivutuksen?= =?UTF-8?q?=20resetointi=20muutettaessa=20hakuparametreja?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/haku-filters.tsx | 172 ++++++++++++++++++++-------- src/app/hooks/usePrevious.ts | 9 ++ 2 files changed, 135 insertions(+), 46 deletions(-) create mode 100644 src/app/hooks/usePrevious.ts diff --git a/src/app/components/haku-filters.tsx b/src/app/components/haku-filters.tsx index cfedc008..69a939e0 100644 --- a/src/app/components/haku-filters.tsx +++ b/src/app/components/haku-filters.tsx @@ -1,5 +1,5 @@ 'use client'; -import React, { ChangeEvent, useMemo } from 'react'; +import React, { ChangeEvent, useCallback, useEffect, useMemo } from 'react'; import { styled, @@ -12,6 +12,7 @@ import { Checkbox, FormControlLabel, Pagination, + Typography, } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; @@ -27,7 +28,8 @@ import { getTranslation } from '../lib/common'; import { useSuspenseQuery } from '@tanstack/react-query'; import { getHaut } from '../lib/kouta'; import useQueryParams from '@/app/hooks/useQueryParams'; -import { useDebounce } from '../hooks/useDebounce'; +import { useDebounce } from '@/app//hooks/useDebounce'; +import { usePrevious } from '@/app/hooks/usePrevious'; const alkamisKausiMatchesSelected = ( haku: Haku, @@ -76,6 +78,8 @@ const useQueryParamState = (name: string, emptyValue?: T) => { const { queryParams, setQueryParam, removeQueryParam } = useQueryParams(); const value = queryParams.get(name) ?? defaultValue; + const previousValue = usePrevious(value); + const setValue = (value?: string) => { if (!value || value === defaultValue) { removeQueryParam(name); @@ -84,52 +88,84 @@ const useQueryParamState = (name: string, emptyValue?: T) => { } }; - return [value, setValue] as const; + return [value, setValue, value !== previousValue] as const; }; -const HakuFiltersInternal = ({ - haut, - hakutavat, -}: { - haut: Haku[]; - hakutavat: Koodi[]; -}) => { - const [search, setSearch] = useQueryParamState('search', ''); +const useHakuSearch = ( + haut: Array, + alkamiskaudet: Array, +) => { + const [searchPhrase, setSearchPhrase, searchPhraseChanged] = + useQueryParamState('search', ''); - const setSearchDebounce = useDebounce(setSearch, 500); + const setSearchDebounce = useDebounce(setSearchPhrase, 500); - const [myosArkistoidut, setMyosArkistoidut] = useQueryParamState( - 'arkistoidut', - 'false', - ); + const [myosArkistoidut, setMyosArkistoidut, myosArkistuidutChanged] = + useQueryParamState('arkistoidut', 'false'); - const [selectedHakutapa, setSelectedHakutapa] = useQueryParamState( - 'hakutapa', - '', + const setMyosArkistoidutBoolean = useCallback( + (value: boolean) => { + setMyosArkistoidut(value ? 'true' : 'false'); + }, + [setMyosArkistoidut], ); - const [selectedAlkamisKausi, setSelectedAlkamisKausi] = useQueryParamState( - 'alkamiskausi', - '', - ); + const [selectedHakutapa, setSelectedHakutapa, selectedHakutapaChanged] = + useQueryParamState('hakutapa', ''); + + const [ + selectedAlkamisKausi, + setSelectedAlkamisKausi, + selectedAlkamisKausiChanged, + ] = useQueryParamState('alkamiskausi', ''); const [page, setPage] = useQueryParamState('page', '1'); + const setPageNum = useCallback( + (pageNum: number) => { + setPage(pageNum.toString()); + }, + [setPage], + ); + const pageNum = parseInt(page, 10); - const [pageSize] = useQueryParamState('page_size', '50'); + const [pageSize, setPageSize] = useQueryParamState('page_size', '50'); - const alkamiskaudet = useMemo(getHakuAlkamisKaudet, []); + const setPageSizeNum = useCallback( + (pageSize: number) => { + setPageSize(pageSize.toString()); + }, + [setPageSize], + ); + + useEffect(() => { + if ( + searchPhraseChanged || + myosArkistuidutChanged || + selectedHakutapaChanged || + selectedAlkamisKausiChanged + ) { + setPageNum(1); + } + }, [ + searchPhraseChanged, + myosArkistuidutChanged, + selectedHakutapaChanged, + selectedAlkamisKausiChanged, + setPageNum, + ]); + + const myosArkistoidutBoolean = myosArkistoidut === ' true'; const results = useMemo(() => { - const tilat = - myosArkistoidut === 'true' - ? [Tila.JULKAISTU, Tila.ARKISTOITU] - : [Tila.JULKAISTU]; + const tilat = myosArkistoidutBoolean + ? [Tila.JULKAISTU, Tila.ARKISTOITU] + : [Tila.JULKAISTU]; return haut.filter( (haku: Haku) => tilat.includes(haku.tila) && - getTranslation(haku.nimi).toLowerCase().includes(search) && + getTranslation(haku.nimi).toLowerCase().includes(searchPhrase) && alkamisKausiMatchesSelected( haku, alkamiskaudet.find((k) => k.value === selectedAlkamisKausi), @@ -138,8 +174,8 @@ const HakuFiltersInternal = ({ ); }, [ haut, - search, - myosArkistoidut, + searchPhrase, + myosArkistoidutBoolean, selectedAlkamisKausi, selectedHakutapa, alkamiskaudet, @@ -152,15 +188,57 @@ const HakuFiltersInternal = ({ return results.slice(start, start + pageSizeNum); }, [results, pageNum, pageSizeNum]); - console.log({ pageResults, pageSizeNum, pageNum }); + return { + searchPhrase, + setSearchPhrase: setSearchDebounce, + myosArkistoidut, + setMyosArkistoidut: setMyosArkistoidutBoolean, + selectedHakutapa, + setSelectedHakutapa, + selectedAlkamisKausi, + setSelectedAlkamisKausi, + page: pageNum, + setPage: setPageNum, + pageSize, + setPageSize: setPageSizeNum, + pageCount: Math.ceil(results.length / pageSizeNum), + pageResults, + results, + }; +}; + +const HakuFiltersInternal = ({ + haut, + hakutavat, +}: { + haut: Haku[]; + hakutavat: Koodi[]; +}) => { + const alkamiskaudet = useMemo(getHakuAlkamisKaudet, []); + + const { + searchPhrase, + setSearchPhrase, + selectedAlkamisKausi, + setSelectedAlkamisKausi, + selectedHakutapa, + setSelectedHakutapa, + myosArkistoidut, + setMyosArkistoidut, + page, + setPage, + pageCount, + results, + pageResults, + } = useHakuSearch(haut, alkamiskaudet); const handleSearchChange = (e: ChangeEvent) => { const searchStr = e.target.value.trim().toLowerCase(); - setSearchDebounce(searchStr); + setSearchPhrase(searchStr); }; const toggleMyosArkistoidut = (_e: unknown, checked: boolean) => { - setMyosArkistoidut(checked ? 'true' : 'false'); + setMyosArkistoidut(checked); }; const changeHakutapa = (e: SelectChangeEvent) => { @@ -184,8 +262,8 @@ const HakuFiltersInternal = ({ Valitse... {alkamiskaudet.map((kausi) => { @@ -259,22 +337,24 @@ const HakuFiltersInternal = ({

Ei hakutuloksia

) : ( <> -

Hakuja: {results.length}

+ + Hakuja: {results.length} + { - setPage(value.toString()); + setPage(value); }} /> { - setPage(value.toString()); + setPage(value); }} /> diff --git a/src/app/hooks/usePrevious.ts b/src/app/hooks/usePrevious.ts new file mode 100644 index 00000000..e8e04bcf --- /dev/null +++ b/src/app/hooks/usePrevious.ts @@ -0,0 +1,9 @@ +import { useEffect, useRef } from 'react'; + +export function usePrevious(value: T) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +} From e030a9247defcfe63323b692c37c45e67c2710fd Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:03:41 +0300 Subject: [PATCH 05/36] =?UTF-8?q?Refaktoroitu=20haku-listauksen=20sivutus-?= =?UTF-8?q?kontrollit=20erilliseen=20komponenttiin=20ja=20lis=C3=A4tty=20s?= =?UTF-8?q?ivun=20koon=20asettaminen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/haku-filters.tsx | 140 +++++++++++++++++++--------- src/app/theme.tsx | 5 + 2 files changed, 102 insertions(+), 43 deletions(-) diff --git a/src/app/components/haku-filters.tsx b/src/app/components/haku-filters.tsx index 69a939e0..c6b3e795 100644 --- a/src/app/components/haku-filters.tsx +++ b/src/app/components/haku-filters.tsx @@ -13,6 +13,7 @@ import { FormControlLabel, Pagination, Typography, + Box, } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; @@ -91,6 +92,10 @@ const useQueryParamState = (name: string, emptyValue?: T) => { return [value, setValue, value !== previousValue] as const; }; +const PAGE_SIZES = [10, 20, 30, 50, 100]; + +const DEFAULT_PAGE_SIZE = 30; + const useHakuSearch = ( haut: Array, alkamiskaudet: Array, @@ -130,7 +135,10 @@ const useHakuSearch = ( const pageNum = parseInt(page, 10); - const [pageSize, setPageSize] = useQueryParamState('page_size', '50'); + const [pageSize, setPageSize] = useQueryParamState( + 'page_size', + DEFAULT_PAGE_SIZE.toString(), + ); const setPageSizeNum = useCallback( (pageSize: number) => { @@ -156,7 +164,7 @@ const useHakuSearch = ( setPageNum, ]); - const myosArkistoidutBoolean = myosArkistoidut === ' true'; + const myosArkistoidutBoolean = myosArkistoidut === 'true'; const results = useMemo(() => { const tilat = myosArkistoidutBoolean @@ -199,14 +207,85 @@ const useHakuSearch = ( setSelectedAlkamisKausi, page: pageNum, setPage: setPageNum, - pageSize, + pageSize: pageSizeNum, setPageSize: setPageSizeNum, - pageCount: Math.ceil(results.length / pageSizeNum), pageResults, results, }; }; +type HakuListFrameProps = { + totalCount: number; + pageNumber: number; + setPageNumber: (page: number) => void; + pageSize: number; + setPageSize: (page: number) => void; + children: React.ReactNode; +}; + +const StyledPagination = styled(Pagination)({ + display: 'flex', +}); + +const HakuListFrame = ({ + totalCount, + pageNumber, + pageSize, + setPageNumber, + setPageSize, + children, +}: HakuListFrameProps) => { + const pageCount = Math.ceil(totalCount / pageSize); + return totalCount === 0 ? ( +

Ei hakutuloksia

+ ) : ( + <> + + Hakuja: {totalCount} + + Näytä per sivu: + + + + + { + setPageNumber(value); + }} + /> + {children} + { + setPageNumber(value); + }} + /> + + + ); +}; + const HakuFiltersInternal = ({ haut, hakutavat, @@ -227,7 +306,8 @@ const HakuFiltersInternal = ({ setMyosArkistoidut, page, setPage, - pageCount, + pageSize, + setPageSize, results, pageResults, } = useHakuSearch(haut, alkamiskaudet); @@ -254,10 +334,7 @@ const HakuFiltersInternal = ({
- + Hae hakuja - + Hakutapa @@ -349,7 +445,7 @@ const HakuFiltersInternal = ({ - Valitse... - {hakutavat.map((tapa) => { - return ( - - {tapa.nimi.fi} - - ); //TODO: translate - })} - - - - - - - Koulutuksen alkamiskausi - - - - - + <> + + + Hae hakuja + + + + + + } + /> + + } + /> + + + Hakutapa + + + + + Koulutuksen alkamiskausi + + + + -
+ ); }; From eebed746b27e588d6a529e38795b926ccabf01fc Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:04:17 +0300 Subject: [PATCH 11/36] =?UTF-8?q?Parannettu=20buttonin=20oletustyylej?= =?UTF-8?q?=C3=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/theme.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/theme.tsx b/src/app/theme.tsx index 2e5e72fd..81094e4c 100644 --- a/src/app/theme.tsx +++ b/src/app/theme.tsx @@ -61,6 +61,9 @@ const theme = createTheme({ }, }, MuiButton: { + defaultProps: { + disableRipple: true, + }, styleOverrides: { root: { boxShadow: 'none', @@ -100,11 +103,14 @@ const theme = createTheme({ text: ({ ownerState, theme }) => { return { color: getColorByName(ownerState.color, theme, 'main'), + background: 'none', '&:hover': { color: getColorByName(ownerState.color, theme, 'light'), + background: 'none', }, '&:active': { color: getColorByName(ownerState.color, theme, 'dark'), + background: 'none', }, }; }, From fb00ec5b3d034a37315ea111bc49ec144431bbf9 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:04:17 +0300 Subject: [PATCH 12/36] Ajetaan saavutettavuustarkistuksia devatessa --- package-lock.json | 26 ++++++++++++++++++++++++++ package.json | 3 ++- src/app/layout.tsx | 4 ++++ src/app/lib/checkAccessibility.ts | 13 +++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/app/lib/checkAccessibility.ts diff --git a/package-lock.json b/package-lock.json index bea830aa..a40f6e2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@axe-core/playwright": "^4.9.0", + "@axe-core/react": "^4.9.0", "@playwright/test": "^1.42.0", "@testing-library/react": "^14.2.2", "@types/cookie": "^0.6.0", @@ -91,6 +92,25 @@ "node": ">=4" } }, + "node_modules/@axe-core/react": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@axe-core/react/-/react-4.9.0.tgz", + "integrity": "sha512-xtqnkFcdxT/T6JD9/hc5Wzv15+m0Qj6VaQCebeIBEveZBOY9nfD6/JIuYuCgWLrgjX+TFgb74nni8XMJvAhVMA==", + "dev": true, + "dependencies": { + "axe-core": "~4.9.0", + "requestidlecallback": "^0.3.0" + } + }, + "node_modules/@axe-core/react/node_modules/axe-core": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.9.1.tgz", + "integrity": "sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", @@ -7659,6 +7679,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/requestidlecallback": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/requestidlecallback/-/requestidlecallback-0.3.0.tgz", + "integrity": "sha512-TWHFkT7S9p7IxLC5A1hYmAYQx2Eb9w1skrXmQ+dS1URyvR8tenMLl4lHbqEOUnpEYxNKpkVMXUgknVpBZWXXfQ==", + "dev": true + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index 4d3445b3..57d86d6d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "touch .env.development.local && NODE_EXTRA_CA_CERTS=\"$(mkcert -CAROOT)/rootCA.pem\" node --env-file=.env --env-file=.env.development.local dev-server.mjs", - "dev-test": "APP_URL=https://localhost:3404 VIRKAILIJA_URL=http://localhost:3104 npm run dev", + "dev-test": "TEST=true APP_URL=https://localhost:3404 VIRKAILIJA_URL=http://localhost:3104 npm run dev", "build": "next build; cp run.sh .next/standalone", "start": "NODE_ENV=production npm run dev", "lint": "next lint", @@ -32,6 +32,7 @@ }, "devDependencies": { "@axe-core/playwright": "^4.9.0", + "@axe-core/react": "^4.9.0", "@playwright/test": "^1.42.0", "@testing-library/react": "^14.2.2", "@types/cookie": "^0.6.0", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e18137cd..e67dd00e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import './globals.css'; @@ -6,6 +7,7 @@ import Wrapper from './wrapper'; import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter'; import { ThemeProvider } from '@mui/material/styles'; import theme from '@/app/theme'; +import { checkAccessibility } from './lib/checkAccessibility'; const inter = Inter({ subsets: ['latin'] }); @@ -33,3 +35,5 @@ export default async function RootLayout({ ); } + +checkAccessibility(); \ No newline at end of file diff --git a/src/app/lib/checkAccessibility.ts b/src/app/lib/checkAccessibility.ts new file mode 100644 index 00000000..89372371 --- /dev/null +++ b/src/app/lib/checkAccessibility.ts @@ -0,0 +1,13 @@ +import React from 'react'; + +export function checkAccessibility() { + if ( + typeof window !== 'undefined' && + process.env.NODE_ENV !== 'production' && + process.env.TEST !== 'true' + ) { + Promise.all([import('@axe-core/react'), import('react-dom')]).then( + ([axe, ReactDOM]) => axe.default(React, ReactDOM, 1000), + ); + } +} From 893596f92bdbc211505713aad33a4c51979faa4c Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:04:17 +0300 Subject: [PATCH 13/36] =?UTF-8?q?Ajetaan=20playwright-testit=20CI:ss=C3=A4?= =?UTF-8?q?=20prod-buildia=20vasten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 4 +++- next.config.mjs | 7 ++++++- package.json | 2 ++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 74569660..266f225c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,8 +50,10 @@ jobs: sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert - name: Create dev certificates run: npm run create-dev-certs + - name: Build app + run: npm run build-test - name: Start the app - run: npm run dev-test & + run: npm run start-test & - name: Run Playwright tests run: npx playwright test - uses: actions/upload-artifact@v4 diff --git a/next.config.mjs b/next.config.mjs index f0b5e5c3..82c946c2 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -16,10 +16,15 @@ const cspHeader = ` const isProd = process.env.NODE_ENV === 'production'; +const isStandalone = process.env.NEXT_OUTPUT_STANDALONE === 'true' ?? isProd; + const basePath = '/valintojen-toteuttaminen'; const nextConfig = { basePath, + eslint: { + ignoreDuringBuilds: true, + }, async headers() { return [ { @@ -38,7 +43,7 @@ const nextConfig = { VIRKAILIJA_URL: process.env.VIRKAILIJA_URL, APP_URL: process.env.APP_URL, }, - output: isProd ? 'standalone' : undefined, + output: isStandalone ? 'standalone' : undefined, }; export default nextConfig; diff --git a/package.json b/package.json index 57d86d6d..833752bc 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "dev": "touch .env.development.local && NODE_EXTRA_CA_CERTS=\"$(mkcert -CAROOT)/rootCA.pem\" node --env-file=.env --env-file=.env.development.local dev-server.mjs", "dev-test": "TEST=true APP_URL=https://localhost:3404 VIRKAILIJA_URL=http://localhost:3104 npm run dev", "build": "next build; cp run.sh .next/standalone", + "build-test": "APP_URL=https://localhost:3404 VIRKAILIJA_URL=http://localhost:3104 NEXT_OUTPUT_STANDALONE=false next build", "start": "NODE_ENV=production npm run dev", + "start-test": "NODE_ENV=production npm run dev-test", "lint": "next lint", "prettier-eslint:fix": "prettier-eslint \"**/*.{js,ts,mjs,cjs,jsx,tsx}\" --write", "test:ui": "playwright test", From bb9621bfcf3bfae6c37115c78855a081e6a9873b Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:04:17 +0300 Subject: [PATCH 14/36] =?UTF-8?q?Poistettu=20turhia=20juttuja=20haku-filte?= =?UTF-8?q?rs:st=C3=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/haku-filters.tsx | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/app/components/haku-filters.tsx b/src/app/components/haku-filters.tsx index c558d46c..1cf947f2 100644 --- a/src/app/components/haku-filters.tsx +++ b/src/app/components/haku-filters.tsx @@ -15,7 +15,6 @@ import { Typography, Box, InputAdornment, - IconButton, } from '@mui/material'; import { @@ -227,8 +226,6 @@ const useHakuSearch = ( return results.slice(start, start + pageSize); }, [results, page, pageSize]); - console.log({ results, pageResults, pageSize, page }); - return { searchPhrase, setSearchPhrase: setSearchDebounce, @@ -299,14 +296,6 @@ const HakuListFrame = ({ - { - setPageNumber(value); - }} - /> {children} - - - + } /> From dc15e05279bed3c7af6e80616c0cb89828e88291 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:04:17 +0300 Subject: [PATCH 15/36] =?UTF-8?q?Pakota=20dynaaminen=20render=C3=B6inti=20?= =?UTF-8?q?etusivulle,=20jotta=20hakutavat=20haetaan=20ajoaikana,=20eik?= =?UTF-8?q?=C3=A4=20k=C3=A4=C3=A4nn=C3=B6saikana.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 19a6f0c4..d6b737a1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,3 @@ -'use server'; import { getHakutavat } from './lib/koodisto'; import AccessTimeIcon from '@mui/icons-material/AccessTime'; import { CSSProperties } from 'react'; @@ -15,6 +14,8 @@ const titleSectionStyle: CSSProperties = { padding: 0, }; +export const dynamic = 'force-dynamic'; + export default async function Home() { const hakutavat = await getHakutavat(); From 667de2bf6d5aef25321b64e92d28d16c88c6f6e3 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:04:17 +0300 Subject: [PATCH 16/36] =?UTF-8?q?Pieni=C3=A4=20parannuksia=20haku-listauks?= =?UTF-8?q?een=20ja=20lis=C3=A4tty=20playwright-testej=C3=A4=20url-paramet?= =?UTF-8?q?reille=20ja=20sorttaukselle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/haku-filters.tsx | 10 ++--- src/app/lib/kouta-types.ts | 4 +- tests/e2e/haut.spec.ts | 65 +++++++++++++++++++++-------- tests/e2e/index.spec.ts | 2 +- tests/e2e/playwright-utils.ts | 11 +++++ 5 files changed, 65 insertions(+), 27 deletions(-) diff --git a/src/app/components/haku-filters.tsx b/src/app/components/haku-filters.tsx index 1cf947f2..9eac7c67 100644 --- a/src/app/components/haku-filters.tsx +++ b/src/app/components/haku-filters.tsx @@ -201,7 +201,7 @@ const useHakuSearch = ( tilat.includes(haku.tila) && getTranslation(haku.nimi) .toLowerCase() - .includes(searchPhrase ?? '') && + .includes(searchPhrase?.toLowerCase() ?? '') && alkamisKausiMatchesSelected( haku, alkamiskaudet.find((k) => k.value === selectedAlkamisKausi), @@ -298,7 +298,7 @@ const HakuListFrame = ({ {children} { @@ -339,8 +339,7 @@ const HakuFiltersInternal = ({ } = useHakuSearch(haut, alkamiskaudet); const handleSearchChange = (e: ChangeEvent) => { - const searchStr = e.target.value.trim().toLowerCase(); - setSearchPhrase(searchStr); + setSearchPhrase(e.target.value); }; const toggleMyosArkistoidut = (_e: unknown, checked: boolean) => { @@ -348,8 +347,7 @@ const HakuFiltersInternal = ({ }; const changeHakutapa = (e: SelectChangeEvent) => { - const tapaKoodiUri = e.target.value; - setSelectedHakutapa(tapaKoodiUri); + setSelectedHakutapa(e.target.value); }; const changeAlkamisKausi = (e: SelectChangeEvent) => { diff --git a/src/app/lib/kouta-types.ts b/src/app/lib/kouta-types.ts index cf67e489..7b93765e 100644 --- a/src/app/lib/kouta-types.ts +++ b/src/app/lib/kouta-types.ts @@ -21,13 +21,13 @@ export const getHakuAlkamisKaudet = (): HaunAlkaminen[] => { alkamisVuosi: i, alkamisKausiKoodiUri: 'kausi_s', alkamisKausiNimi: 'SYKSY', - value: `syksy_${i}`, + value: `${i}_syksy`, }); alkamiset.push({ alkamisVuosi: i, alkamisKausiKoodiUri: 'kausi_k', alkamisKausiNimi: 'KEVÄT', - value: `kevat_${i}`, + value: `${i}_kevat`, }); } return alkamiset; diff --git a/tests/e2e/haut.spec.ts b/tests/e2e/haut.spec.ts index b117d816..562dc5e9 100644 --- a/tests/e2e/haut.spec.ts +++ b/tests/e2e/haut.spec.ts @@ -1,7 +1,8 @@ -import { test, expect, Page } from '@playwright/test'; +import { test, expect, Page, Locator } from '@playwright/test'; import { expectAllSpinnersHidden, expectPageAccessibilityOk, + expectUrlParamToEqual, } from './playwright-utils'; async function selectHakutapa(page: Page, expectedOption: string) { @@ -40,51 +41,79 @@ test('Haku-page accessibility', async ({ page }) => { const getMyosArkistoidut = (page: Page) => page.getByRole('checkbox', { name: 'Myös arkistoidut' }); +const getTableRows = (loc: Page | Locator) => loc.locator('tbody tr'); + test('filters haku by published state', async ({ page }) => { await expect(getMyosArkistoidut(page)).not.toBeChecked(); - await expect(page.locator('tbody tr')).toHaveCount(3); + const tableRows = getTableRows(page); + await expect(tableRows).toHaveCount(3); const hakuInput = await page.getByRole('textbox', { name: 'Hae hakuja' }); hakuInput.fill('Luk'); - await expect(page.locator('tbody tr')).toHaveCount(1); - await expect(page.locator('tbody tr')).toContainText( - 'Hausjärven lukio jatkuva haku', - ); + await expect(tableRows).toHaveCount(1); + await expect(tableRows).toContainText('Hausjärven lukio jatkuva haku'); }); test('filters haku by archived state', async ({ page }) => { + const tableRows = getTableRows(page); const myosArkistoidut = getMyosArkistoidut(page); await myosArkistoidut.click(); + await expect(myosArkistoidut).toBeChecked(); - await expect(page.locator('tbody tr')).toHaveCount(6); + await expect(tableRows).toHaveCount(6); const hakuInput = await page.getByRole('textbox', { name: 'Hae hakuja' }); hakuInput.fill('hak'); - await expect(page.locator('tbody tr')).toHaveCount(5); + await expect(tableRows).toHaveCount(5); hakuInput.fill('Leppä'); - await expect(page.locator('tbody tr')).toHaveCount(1); - await expect(page.locator('tbody tr')).toContainText( - 'Leppävirran lukio - Jatkuva haku', - ); + await expect(tableRows).toHaveCount(1); + await expect(tableRows).toContainText('Leppävirran lukio - Jatkuva haku'); + await expectUrlParamToEqual(page, 'search', 'Leppä'); }); test('filters by hakutapa', async ({ page }) => { + const tableRows = getTableRows(page); await selectHakutapa(page, 'Erillishaku'); - await expect(page.locator('tbody tr')).toHaveCount(1); + await expect(tableRows).toHaveCount(1); + await expectUrlParamToEqual(page, 'hakutapa', 'hakutapa_02'); await selectHakutapa(page, 'Jatkuva haku'); - await expect(page.locator('tbody tr')).toHaveCount(2); + await expect(tableRows).toHaveCount(2); + await expectUrlParamToEqual(page, 'hakutapa', 'hakutapa_03'); }); test('filters by start period', async ({ page }) => { + const tableRows = getTableRows(page); await selectKausi(page, '2024 SYKSY'); - await expect(page.locator('tbody tr')).toHaveCount(1); + await expect(tableRows).toHaveCount(1); await selectKausi(page, '2020 SYKSY'); - await expect(page.locator('tbody tr')).toHaveCount(0); + await expect(tableRows).toHaveCount(0); + await expectUrlParamToEqual(page, 'alkamiskausi', '2020_syksy'); }); test('filters by hakutapa and start period', async ({ page }) => { + const tableRows = getTableRows(page); await selectHakutapa(page, 'Jatkuva haku'); - await expect(page.locator('tbody tr')).toHaveCount(2); + await expect(tableRows).toHaveCount(2); await selectKausi(page, '2023 SYKSY'); - await expect(page.locator('tbody tr')).toHaveCount(1); + await expect(tableRows).toHaveCount(1); +}); + +test('sorts list by nimi when header clicked', async ({ page }) => { + const nimiHeader = page.getByRole('columnheader', { name: 'Nimi' }); + await nimiHeader.getByRole('button').click(); + await expect(nimiHeader).toHaveAttribute('aria-sort', 'ascending'); + await expectUrlParamToEqual(page, 'sort', 'nimi:asc'); + + await nimiHeader.getByRole('button').click(); + + await expect(nimiHeader).toHaveAttribute('aria-sort', 'descending'); + await expectUrlParamToEqual(page, 'sort', 'nimi:desc'); + + const tableRows = getTableRows(page); + + await expect( + tableRows.first().getByRole('cell', { + name: 'Tampere University Separate Admission/ Finnish MAOL Competition Route 2024', + }), + ).toBeVisible(); }); test('navigates to haku page', async ({ page }) => { diff --git a/tests/e2e/index.spec.ts b/tests/e2e/index.spec.ts index 310223d0..0e25bb70 100644 --- a/tests/e2e/index.spec.ts +++ b/tests/e2e/index.spec.ts @@ -7,7 +7,7 @@ import { test('index accessibility', async ({ page }) => { await page.goto('/'); await expectAllSpinnersHidden(page); - await page.locator('tr').nth(1).hover(); + await page.locator('tbody tr').nth(1).hover(); await expectPageAccessibilityOk(page); }); diff --git a/tests/e2e/playwright-utils.ts b/tests/e2e/playwright-utils.ts index 3181f600..907508b8 100644 --- a/tests/e2e/playwright-utils.ts +++ b/tests/e2e/playwright-utils.ts @@ -10,3 +10,14 @@ export const expectAllSpinnersHidden = async (page: Page) => { const spinners = page.getByRole('progressbar'); await expect(spinners).toHaveCount(0); }; + +export const expectUrlParamToEqual = async ( + page: Page, + paramName: string, + value: string, +) => { + const pageURL = await page.url(); + const urlObj = new URL(pageURL); + const param = urlObj.searchParams.get(paramName); + await expect(param).toEqual(value); +}; From 2511045e2edcacb964390bb0087d0d7fc0085746 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:04:17 +0300 Subject: [PATCH 17/36] Vitest testikattavuusmittaus --- package-lock.json | 418 +++++++++++++++++++++++++++++----------------- package.json | 2 + vitest.config.ts | 5 + 3 files changed, 270 insertions(+), 155 deletions(-) diff --git a/package-lock.json b/package-lock.json index a40f6e2f..17ba3032 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@typescript-eslint/eslint-plugin": "^7.7.0", "@typescript-eslint/parser": "^7.7.0", "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-istanbul": "^1.6.0", "autoprefixer": "^10.0.1", "eslint": "^8", "eslint-config-next": "14.1.0", @@ -133,27 +134,26 @@ } }, "node_modules/@babel/core": { - "version": "7.22.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.1.tgz", - "integrity": "sha512-Hkqu7J4ynysSXxmAahpN1jjRwVJ+NdpraFLIWflgjpVob3KNyK3/tIUc7Q7szed8WMp0JNa7Qtd1E9Oo22F9gA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.22.0", - "@babel/helper-compilation-targets": "^7.22.1", - "@babel/helper-module-transforms": "^7.22.1", - "@babel/helpers": "^7.22.0", - "@babel/parser": "^7.22.0", - "@babel/template": "^7.21.9", - "@babel/traverse": "^7.22.1", - "@babel/types": "^7.22.0", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -163,12 +163,17 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/@babel/core/node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "peer": true, "bin": { "json5": "lib/cli.js" }, @@ -181,18 +186,17 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "peer": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz", - "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", "dev": true, "dependencies": { - "@babel/types": "^7.24.0", + "@babel/types": "^7.24.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -276,27 +280,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -315,41 +319,41 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", "engines": { "node": ">=6.9.0" } @@ -364,14 +368,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.1.tgz", - "integrity": "sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", "dev": true, "dependencies": { "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -456,9 +460,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", - "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -523,19 +527,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", - "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.1", - "@babel/generator": "^7.24.1", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -553,12 +557,12 @@ } }, "node_modules/@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1241,6 +1245,15 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -2739,71 +2752,37 @@ "vite": "^4.2.0 || ^5.0.0" } }, - "node_modules/@vitejs/plugin-react/node_modules/@babel/core": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz", - "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==", + "node_modules/@vitest/coverage-istanbul": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-1.6.0.tgz", + "integrity": "sha512-h/BwpXehkkS0qsNCS00QxiupAqVkNi0WT19BR0dQvlge5oHghoSVLx63fABYFoKxVb7Ue7+k6V2KokmQ1zdMpg==", "dev": true, "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.1", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.1", - "@babel/parser": "^7.24.1", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-instrument": "^6.0.1", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.4", + "istanbul-reports": "^3.1.6", + "magicast": "^0.3.3", + "picocolors": "^1.0.0", + "test-exclude": "^6.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@vitejs/plugin-react/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/@vitejs/plugin-react/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" + "url": "https://opencollective.com/vitest" }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@vitejs/plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "peerDependencies": { + "vitest": "1.6.0" } }, "node_modules/@vitest/expect": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz", - "integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", + "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", "dev": true, "dependencies": { - "@vitest/spy": "1.4.0", - "@vitest/utils": "1.4.0", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", "chai": "^4.3.10" }, "funding": { @@ -2811,12 +2790,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz", - "integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", + "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", "dev": true, "dependencies": { - "@vitest/utils": "1.4.0", + "@vitest/utils": "1.6.0", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -2852,9 +2831,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz", - "integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", + "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -2866,9 +2845,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz", - "integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", + "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -2878,9 +2857,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz", - "integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -5491,6 +5470,12 @@ "node": ">=18" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -6022,6 +6007,72 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", + "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", @@ -6677,15 +6728,38 @@ } }, "node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/magicast": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/make-plural": { @@ -8436,6 +8510,40 @@ "node": ">=6" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -8802,9 +8910,9 @@ } }, "node_modules/vite-node": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz", - "integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", + "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -8824,16 +8932,16 @@ } }, "node_modules/vitest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz", - "integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", + "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", "dev": true, "dependencies": { - "@vitest/expect": "1.4.0", - "@vitest/runner": "1.4.0", - "@vitest/snapshot": "1.4.0", - "@vitest/spy": "1.4.0", - "@vitest/utils": "1.4.0", + "@vitest/expect": "1.6.0", + "@vitest/runner": "1.6.0", + "@vitest/snapshot": "1.6.0", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -8845,9 +8953,9 @@ "std-env": "^3.5.0", "strip-literal": "^2.0.0", "tinybench": "^2.5.1", - "tinypool": "^0.8.2", + "tinypool": "^0.8.3", "vite": "^5.0.0", - "vite-node": "1.4.0", + "vite-node": "1.6.0", "why-is-node-running": "^2.2.2" }, "bin": { @@ -8862,8 +8970,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.4.0", - "@vitest/ui": "1.4.0", + "@vitest/browser": "1.6.0", + "@vitest/ui": "1.6.0", "happy-dom": "*", "jsdom": "*" }, diff --git a/package.json b/package.json index 833752bc..274e5033 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "prettier-eslint:fix": "prettier-eslint \"**/*.{js,ts,mjs,cjs,jsx,tsx}\" --write", "test:ui": "playwright test", "test": "vitest", + "coverage": "vitest run --coverage", "create-dev-certs": "mkdir -p certificates && cd certificates && mkcert localhost && mkcert -install", "prepare": "husky", "typecheck": "tsc" @@ -44,6 +45,7 @@ "@typescript-eslint/eslint-plugin": "^7.7.0", "@typescript-eslint/parser": "^7.7.0", "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-istanbul": "^1.6.0", "autoprefixer": "^10.0.1", "eslint": "^8", "eslint-config-next": "14.1.0", diff --git a/vitest.config.ts b/vitest.config.ts index 77a1f133..0fdc922c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,5 +6,10 @@ export default defineConfig({ test: { environment: 'jsdom', dir: './tests/unit', + coverage: { + provider: 'istanbul', + include: ['src/**'], + }, + exclude: ['./cdk'], }, }); From 241a0f2f37ce3e8e26d5408a5c30826ebe4f353b Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:05:22 +0300 Subject: [PATCH 18/36] Yhdenmukaistettu ikonien importit --- src/app/components/header.tsx | 2 +- src/app/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/header.tsx b/src/app/components/header.tsx index 56fd6f01..c5cd3db9 100644 --- a/src/app/components/header.tsx +++ b/src/app/components/header.tsx @@ -1,5 +1,5 @@ import React, { CSSProperties } from 'react'; -import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'; +import { HomeOutlined as HomeOutlinedIcon } from '@mui/icons-material'; import { Link as MuiLink } from '@mui/material'; export type HeaderProps = { diff --git a/src/app/page.tsx b/src/app/page.tsx index d6b737a1..19e4f6b9 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,5 @@ import { getHakutavat } from './lib/koodisto'; -import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import { AccessTime as AccessTimeIcon } from '@mui/icons-material'; import { CSSProperties } from 'react'; import HakuFilters from './components/haku-filters'; import Header from './components/header'; From b00e1e49b23cbe8a37a65e351efe81fc4795e801 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:05:23 +0300 Subject: [PATCH 19/36] =?UTF-8?q?Poistettu=20key-attribuutti=20haku-inputi?= =?UTF-8?q?sta,=20jottei=20sit=C3=A4=20render=C3=B6id=C3=A4=20turhaan=20ja?= =?UTF-8?q?=20menetet=C3=A4=20kursorin=20sijaintia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/haku-filters.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/components/haku-filters.tsx b/src/app/components/haku-filters.tsx index 9eac7c67..fbe8f134 100644 --- a/src/app/components/haku-filters.tsx +++ b/src/app/components/haku-filters.tsx @@ -374,7 +374,6 @@ const HakuFiltersInternal = ({ Date: Mon, 13 May 2024 10:13:57 +0300 Subject: [PATCH 20/36] =?UTF-8?q?Poistetaan=20hakujen=20suodatuksen=20para?= =?UTF-8?q?metri=20URL:st=C3=A4,=20kun=20valitaan=20default-arvo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/haku-filters.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/components/haku-filters.tsx b/src/app/components/haku-filters.tsx index fbe8f134..5b4d6808 100644 --- a/src/app/components/haku-filters.tsx +++ b/src/app/components/haku-filters.tsx @@ -125,6 +125,7 @@ const DEFAULT_PAGE_SIZE = 30; const DEFAULT_NUQS_OPTIONS = { history: 'push', clearOnDefault: true, + defaultValue: '', } as const; const useHakuSearch = ( @@ -140,7 +141,7 @@ const useHakuSearch = ( const [myosArkistoidut, setMyosArkistoidut] = useQueryState( 'arkistoidut', - parseAsBoolean.withDefault(false).withOptions(DEFAULT_NUQS_OPTIONS), + parseAsBoolean.withOptions(DEFAULT_NUQS_OPTIONS).withDefault(false), ); const [selectedHakutapa, setSelectedHakutapa] = useQueryState( @@ -155,14 +156,14 @@ const useHakuSearch = ( const [page, setPage] = useQueryState( 'page', - parseAsInteger.withDefault(1).withOptions(DEFAULT_NUQS_OPTIONS), + parseAsInteger.withOptions(DEFAULT_NUQS_OPTIONS).withDefault(1), ); const [pageSize, setPageSize] = useQueryState( 'page_size', parseAsInteger - .withDefault(DEFAULT_PAGE_SIZE) - .withOptions(DEFAULT_NUQS_OPTIONS), + .withOptions(DEFAULT_NUQS_OPTIONS) + .withDefault(DEFAULT_PAGE_SIZE), ); const [sort, setSort] = useQueryState('sort', DEFAULT_NUQS_OPTIONS); From b41c17385108e8d94be669e0869ab1c2a20aeb71 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:42:59 +0300 Subject: [PATCH 21/36] =?UTF-8?q?J=C3=A4rjestelty=20HakuFilters-komponenti?= =?UTF-8?q?n=20koodia=20paremmin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/haku-filters.tsx | 270 +----------------- .../haku-table-pagination-wrapper.tsx | 78 +++++ src/app/components/haku-table.tsx | 2 +- src/app/hooks/useHakuSearch.tsx | 145 ++++++++++ src/app/lib/common.ts | 32 +++ 5 files changed, 264 insertions(+), 263 deletions(-) create mode 100644 src/app/components/haku-table-pagination-wrapper.tsx create mode 100644 src/app/hooks/useHakuSearch.tsx diff --git a/src/app/components/haku-filters.tsx b/src/app/components/haku-filters.tsx index 5b4d6808..9cbf36fb 100644 --- a/src/app/components/haku-filters.tsx +++ b/src/app/components/haku-filters.tsx @@ -1,8 +1,7 @@ 'use client'; -import React, { ChangeEvent, useEffect, useMemo } from 'react'; +import React, { ChangeEvent, useMemo } from 'react'; import { - styled, FormControl, Select, MenuItem, @@ -11,68 +10,18 @@ import { OutlinedInput, Checkbox, FormControlLabel, - Pagination, - Typography, Box, InputAdornment, } from '@mui/material'; -import { - Haku, - HaunAlkaminen, - Tila, - getHakuAlkamisKaudet, -} from '../lib/kouta-types'; +import { getHakuAlkamisKaudet } from '../lib/kouta-types'; import { Koodi } from '../lib/koodisto'; -import { HakuList } from './haku-table'; -import { Language, TranslatedName, getTranslation } from '../lib/common'; +import { HakuTable } from './haku-table'; import { useSuspenseQuery } from '@tanstack/react-query'; import { getHaut } from '../lib/kouta'; -import { useDebounce } from '@/app/hooks/useDebounce'; -import { parseAsBoolean, parseAsInteger, useQueryState } from 'nuqs'; -import { useHasChanged } from '@/app/hooks/useHasChanged'; -import { getSortParts } from './table/list-table'; import { Search } from '@mui/icons-material'; - -function isObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null; -} - -function isTranslatedName(value: unknown): value is TranslatedName { - return ( - isObject(value) && - (typeof value?.fi === 'string' || - typeof value?.sv === 'string' || - typeof value?.en === 'string') - ); -} - -const byProp = >( - key: string, - direction: 'asc' | 'desc' = 'asc', - lng: Language, -) => { - const asc = direction === 'asc'; - return (a: T, b: T) => { - const aKey = a[key]; - const aProp = isTranslatedName(aKey) ? getTranslation(aKey, lng) : aKey; - - const bKey = b[key]; - const bProp = isTranslatedName(bKey) ? getTranslation(bKey, lng) : bKey; - - return aProp > bProp ? (asc ? 1 : -1) : bProp > aProp ? (asc ? -1 : 1) : 0; - }; -}; - -const alkamisKausiMatchesSelected = ( - haku: Haku, - selectedAlkamisKausi?: HaunAlkaminen, -): boolean => - !selectedAlkamisKausi || - (haku.alkamisVuosi === selectedAlkamisKausi.alkamisVuosi && - haku.alkamisKausiKoodiUri.startsWith( - selectedAlkamisKausi.alkamisKausiKoodiUri, - )); +import { useHakuSearch } from '../hooks/useHakuSearch'; +import { HakuTablePaginationWrapper } from './haku-table-pagination-wrapper'; const KAUSI_MAPPING = Object.freeze({ kausi_s: { @@ -115,209 +64,6 @@ export const HakuFilters = ({ hakutavat }: { hakutavat: Array }) => { })), }); - return ; -}; - -const PAGE_SIZES = [10, 20, 30, 50, 100]; - -const DEFAULT_PAGE_SIZE = 30; - -const DEFAULT_NUQS_OPTIONS = { - history: 'push', - clearOnDefault: true, - defaultValue: '', -} as const; - -const useHakuSearch = ( - haut: Array, - alkamiskaudet: Array, -) => { - const [searchPhrase, setSearchPhrase] = useQueryState( - 'search', - DEFAULT_NUQS_OPTIONS, - ); - - const setSearchDebounce = useDebounce(setSearchPhrase, 500); - - const [myosArkistoidut, setMyosArkistoidut] = useQueryState( - 'arkistoidut', - parseAsBoolean.withOptions(DEFAULT_NUQS_OPTIONS).withDefault(false), - ); - - const [selectedHakutapa, setSelectedHakutapa] = useQueryState( - 'hakutapa', - DEFAULT_NUQS_OPTIONS, - ); - - const [selectedAlkamisKausi, setSelectedAlkamisKausi] = useQueryState( - 'alkamiskausi', - DEFAULT_NUQS_OPTIONS, - ); - - const [page, setPage] = useQueryState( - 'page', - parseAsInteger.withOptions(DEFAULT_NUQS_OPTIONS).withDefault(1), - ); - - const [pageSize, setPageSize] = useQueryState( - 'page_size', - parseAsInteger - .withOptions(DEFAULT_NUQS_OPTIONS) - .withDefault(DEFAULT_PAGE_SIZE), - ); - - const [sort, setSort] = useQueryState('sort', DEFAULT_NUQS_OPTIONS); - - const myosArkistoidutChanged = useHasChanged(myosArkistoidut); - const searchPhraseChanged = useHasChanged(searchPhrase); - const selectedAlkamisKausiChanged = useHasChanged(selectedAlkamisKausi); - const selectedHakutapaChanged = useHasChanged(selectedHakutapa); - - useEffect(() => { - if ( - searchPhraseChanged || - myosArkistoidutChanged || - selectedHakutapaChanged || - selectedAlkamisKausiChanged - ) { - setPage(1); - } - }, [ - searchPhraseChanged, - myosArkistoidutChanged, - selectedHakutapaChanged, - selectedAlkamisKausiChanged, - setPage, - ]); - - const results = useMemo(() => { - const tilat = myosArkistoidut - ? [Tila.JULKAISTU, Tila.ARKISTOITU] - : [Tila.JULKAISTU]; - - const { orderBy, direction } = getSortParts(sort ?? ''); - - const filtered = haut.filter( - (haku: Haku) => - tilat.includes(haku.tila) && - getTranslation(haku.nimi) - .toLowerCase() - .includes(searchPhrase?.toLowerCase() ?? '') && - alkamisKausiMatchesSelected( - haku, - alkamiskaudet.find((k) => k.value === selectedAlkamisKausi), - ) && - haku.hakutapaKoodiUri.startsWith(selectedHakutapa ?? ''), - ); - return orderBy && direction - ? filtered.sort(byProp(orderBy, direction, Language.FI)) - : filtered; - }, [ - haut, - searchPhrase, - myosArkistoidut, - selectedAlkamisKausi, - selectedHakutapa, - alkamiskaudet, - sort, - ]); - - const pageResults = useMemo(() => { - const start = pageSize * (page - 1); - return results.slice(start, start + pageSize); - }, [results, page, pageSize]); - - return { - searchPhrase, - setSearchPhrase: setSearchDebounce, - myosArkistoidut, - setMyosArkistoidut: setMyosArkistoidut, - selectedHakutapa, - setSelectedHakutapa, - selectedAlkamisKausi, - setSelectedAlkamisKausi, - page, - setPage, - pageSize, - setPageSize, - pageResults, - results, - sort, - setSort, - }; -}; - -type HakuListFrameProps = { - totalCount: number; - pageNumber: number; - setPageNumber: (page: number) => void; - pageSize: number; - setPageSize: (page: number) => void; - children: React.ReactNode; -}; - -const StyledPagination = styled(Pagination)({ - display: 'flex', -}); - -const HakuListFrame = ({ - totalCount, - pageNumber, - pageSize, - setPageNumber, - setPageSize, - children, -}: HakuListFrameProps) => { - const pageCount = Math.ceil(totalCount / pageSize); - return totalCount === 0 ? ( -

Ei hakutuloksia

- ) : ( - <> - - Hakuja: {totalCount} - - Näytä per sivu: - - - - - {children} - { - setPageNumber(value); - }} - /> - - - ); -}; - -const HakuFiltersInternal = ({ - haut, - hakutavat, -}: { - haut: Haku[]; - hakutavat: Koodi[]; -}) => { const alkamiskaudet = useMemo(getHakuAlkamisKaudet, []); const { @@ -439,20 +185,20 @@ const HakuFiltersInternal = ({
- - - + ); }; diff --git a/src/app/components/haku-table-pagination-wrapper.tsx b/src/app/components/haku-table-pagination-wrapper.tsx new file mode 100644 index 00000000..577bbf21 --- /dev/null +++ b/src/app/components/haku-table-pagination-wrapper.tsx @@ -0,0 +1,78 @@ +'use client'; +import React from 'react'; +import { + FormControl, + Select, + MenuItem, + FormLabel, + Typography, + Box, + styled, + Pagination, +} from '@mui/material'; +import { DEFAULT_PAGE_SIZE } from '../hooks/useHakuSearch'; + +export const PAGE_SIZES = [10, 20, 30, 50, 100]; + +export const StyledPagination = styled(Pagination)({ + display: 'flex', +}); + +export type HakuTablePaginationWrapperProps = { + totalCount: number; + pageNumber: number; + setPageNumber: (page: number) => void; + pageSize: number; + setPageSize: (page: number) => void; + children: React.ReactNode; +}; + +export const HakuTablePaginationWrapper = ({ + totalCount, + pageNumber, + pageSize, + setPageNumber, + setPageSize, + children, +}: HakuTablePaginationWrapperProps) => { + return totalCount === 0 ? ( +

Ei hakutuloksia

+ ) : ( + <> + + Hakuja: {totalCount} + + Näytä per sivu: + + + + + {children} + { + setPageNumber(value); + }} + /> + + + ); +}; diff --git a/src/app/components/haku-table.tsx b/src/app/components/haku-table.tsx index f2e74717..320a34b4 100644 --- a/src/app/components/haku-table.tsx +++ b/src/app/components/haku-table.tsx @@ -9,7 +9,7 @@ import ListTable, { } from './table/list-table'; import { Haku } from '../lib/kouta-types'; -export const HakuList = ({ +export const HakuTable = ({ haut, hakutavat, setSort, diff --git a/src/app/hooks/useHakuSearch.tsx b/src/app/hooks/useHakuSearch.tsx new file mode 100644 index 00000000..f00296c9 --- /dev/null +++ b/src/app/hooks/useHakuSearch.tsx @@ -0,0 +1,145 @@ +'use client'; +import { useEffect, useMemo } from 'react'; +import { Haku, HaunAlkaminen, Tila } from '../lib/kouta-types'; +import { Language, byProp, getTranslation } from '../lib/common'; +import { useDebounce } from '@/app/hooks/useDebounce'; +import { parseAsBoolean, parseAsInteger, useQueryState } from 'nuqs'; +import { useHasChanged } from '@/app/hooks/useHasChanged'; +import { getSortParts } from '../components/table/list-table'; + +export const DEFAULT_PAGE_SIZE = 30; + +const DEFAULT_NUQS_OPTIONS = { + history: 'push', + clearOnDefault: true, + defaultValue: '', +} as const; + +export const alkamisKausiMatchesSelected = ( + haku: Haku, + selectedAlkamisKausi?: HaunAlkaminen, +): boolean => + !selectedAlkamisKausi || + (haku.alkamisVuosi === selectedAlkamisKausi.alkamisVuosi && + haku.alkamisKausiKoodiUri.startsWith( + selectedAlkamisKausi.alkamisKausiKoodiUri, + )); + +export const useHakuSearch = ( + haut: Array, + alkamiskaudet: Array, +) => { + const [searchPhrase, setSearchPhrase] = useQueryState( + 'search', + DEFAULT_NUQS_OPTIONS, + ); + + const setSearchDebounce = useDebounce(setSearchPhrase, 500); + + const [myosArkistoidut, setMyosArkistoidut] = useQueryState( + 'arkistoidut', + parseAsBoolean.withOptions(DEFAULT_NUQS_OPTIONS).withDefault(false), + ); + + const [selectedHakutapa, setSelectedHakutapa] = useQueryState( + 'hakutapa', + DEFAULT_NUQS_OPTIONS, + ); + + const [selectedAlkamisKausi, setSelectedAlkamisKausi] = useQueryState( + 'alkamiskausi', + DEFAULT_NUQS_OPTIONS, + ); + + const [page, setPage] = useQueryState( + 'page', + parseAsInteger.withOptions(DEFAULT_NUQS_OPTIONS).withDefault(1), + ); + + const [pageSize, setPageSize] = useQueryState( + 'page_size', + parseAsInteger + .withOptions(DEFAULT_NUQS_OPTIONS) + .withDefault(DEFAULT_PAGE_SIZE), + ); + + const [sort, setSort] = useQueryState('sort', DEFAULT_NUQS_OPTIONS); + + const myosArkistoidutChanged = useHasChanged(myosArkistoidut); + const searchPhraseChanged = useHasChanged(searchPhrase); + const selectedAlkamisKausiChanged = useHasChanged(selectedAlkamisKausi); + const selectedHakutapaChanged = useHasChanged(selectedHakutapa); + + useEffect(() => { + if ( + searchPhraseChanged || + myosArkistoidutChanged || + selectedHakutapaChanged || + selectedAlkamisKausiChanged + ) { + setPage(1); + } + }, [ + searchPhraseChanged, + myosArkistoidutChanged, + selectedHakutapaChanged, + selectedAlkamisKausiChanged, + setPage, + ]); + + const results = useMemo(() => { + const tilat = myosArkistoidut + ? [Tila.JULKAISTU, Tila.ARKISTOITU] + : [Tila.JULKAISTU]; + + const { orderBy, direction } = getSortParts(sort ?? ''); + + const filtered = haut.filter( + (haku: Haku) => + tilat.includes(haku.tila) && + getTranslation(haku.nimi) + .toLowerCase() + .includes(searchPhrase?.toLowerCase() ?? '') && + alkamisKausiMatchesSelected( + haku, + alkamiskaudet.find((k) => k.value === selectedAlkamisKausi), + ) && + haku.hakutapaKoodiUri.startsWith(selectedHakutapa ?? ''), + ); + return orderBy && direction + ? filtered.sort(byProp(orderBy, direction, Language.FI)) + : filtered; + }, [ + haut, + searchPhrase, + myosArkistoidut, + selectedAlkamisKausi, + selectedHakutapa, + alkamiskaudet, + sort, + ]); + + const pageResults = useMemo(() => { + const start = pageSize * (page - 1); + return results.slice(start, start + pageSize); + }, [results, page, pageSize]); + + return { + searchPhrase, + setSearchPhrase: setSearchDebounce, + myosArkistoidut, + setMyosArkistoidut: setMyosArkistoidut, + selectedHakutapa, + setSelectedHakutapa, + selectedAlkamisKausi, + setSelectedAlkamisKausi, + page, + setPage, + pageSize, + setPageSize, + pageResults, + results, + sort, + setSort, + }; +}; diff --git a/src/app/lib/common.ts b/src/app/lib/common.ts index 87680480..cd155708 100644 --- a/src/app/lib/common.ts +++ b/src/app/lib/common.ts @@ -36,3 +36,35 @@ export class FetchError extends Error { this.response = response; } } + +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function isTranslatedName(value: unknown): value is TranslatedName { + return ( + isObject(value) && + (typeof value?.fi === 'string' || + typeof value?.sv === 'string' || + typeof value?.en === 'string') + ); +} + +export const byProp = < + T extends Record, +>( + key: string, + direction: 'asc' | 'desc' = 'asc', + lng: Language, +) => { + const asc = direction === 'asc'; + return (a: T, b: T) => { + const aKey = a[key]; + const aProp = isTranslatedName(aKey) ? getTranslation(aKey, lng) : aKey; + + const bKey = b[key]; + const bProp = isTranslatedName(bKey) ? getTranslation(bKey, lng) : bKey; + + return aProp > bProp ? (asc ? 1 : -1) : bProp > aProp ? (asc ? -1 : 1) : 0; + }; +}; From f80b9edec07f149ca1f8c9631fc796b3768208e0 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:43:53 +0300 Subject: [PATCH 22/36] Siirretty "/"-reitin page ja error "(root)" route grouppiin --- src/app/{ => (root)}/error.tsx | 0 src/app/{ => (root)}/page.tsx | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/app/{ => (root)}/error.tsx (100%) rename src/app/{ => (root)}/page.tsx (84%) diff --git a/src/app/error.tsx b/src/app/(root)/error.tsx similarity index 100% rename from src/app/error.tsx rename to src/app/(root)/error.tsx diff --git a/src/app/page.tsx b/src/app/(root)/page.tsx similarity index 84% rename from src/app/page.tsx rename to src/app/(root)/page.tsx index 19e4f6b9..ab256a86 100644 --- a/src/app/page.tsx +++ b/src/app/(root)/page.tsx @@ -1,8 +1,8 @@ -import { getHakutavat } from './lib/koodisto'; +import { getHakutavat } from '../lib/koodisto'; import { AccessTime as AccessTimeIcon } from '@mui/icons-material'; import { CSSProperties } from 'react'; -import HakuFilters from './components/haku-filters'; -import Header from './components/header'; +import HakuFilters from '../components/haku-filters'; +import Header from '../components/header'; const titleSectionStyle: CSSProperties = { borderBottom: '1px solid rgba(0, 0, 0, 0.15)', From c69c365f5ae1c12bd70f95446b8263266742ab3d Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 10:57:40 +0300 Subject: [PATCH 23/36] =?UTF-8?q?Poistettu=20turhaksi=20j=C3=A4=C3=A4nyt?= =?UTF-8?q?=20useQueryParams?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/hooks/useQueryParams.ts | 56 --------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 src/app/hooks/useQueryParams.ts diff --git a/src/app/hooks/useQueryParams.ts b/src/app/hooks/useQueryParams.ts deleted file mode 100644 index 9ff15b98..00000000 --- a/src/app/hooks/useQueryParams.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { useCallback } from 'react'; - -export default function useQueryParams() { - const searchParams = useSearchParams(); - const router = useRouter(); - const pathname = usePathname(); - - type UrlParam = { - name: string; - value: string; - }; - - const createQueryStrings = useCallback( - (newparams: UrlParam[]) => { - const params = new URLSearchParams(searchParams?.toString() ?? ''); - newparams.map((p) => params.set(p.name, p.value)); - - return params.toString(); - }, - [searchParams], - ); - const createQueryString = useCallback( - (name: string, value: string) => { - const params = new URLSearchParams(searchParams); - params.set(name, value); - return params.toString(); - }, - [searchParams], - ); - - const removeQueryString = useCallback( - (name: string) => { - const params = new URLSearchParams(searchParams); - params.delete(name); - return params.toString(); - }, - [searchParams], - ); - - const setQueryParam = (queryName: string, value: string) => { - router.push(`${pathname}?${createQueryString(queryName, value)}`); - }; - - const removeQueryParam = (queryName: string) => { - router.push(`${pathname}?${removeQueryString(queryName)}`); - }; - - return { - queryParams: searchParams, - createQueryString, - createQueryStrings, - removeQueryParam, - setQueryParam, - }; -} From 9a83cec584036b92b634118b6f6dbeadb152dc11 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 11:41:26 +0300 Subject: [PATCH 24/36] Poistettu turha header-parametri layoutista, joka feilasi next.js:n tyyppitarkistuksen --- src/app/haku/[oid]/hakukohde/[hakukohde]/layout.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/layout.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/layout.tsx index 179fe135..21b820d5 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/layout.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/layout.tsx @@ -5,7 +5,6 @@ export default function HakuLayout({ params, }: { children: React.ReactNode; - header: React.ReactNode; params: { oid: string; hakukohde: string }; }) { return ( From f739ba5be856ea19aeaac7019edca1ff4b719ea3 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 12:38:08 +0300 Subject: [PATCH 25/36] Refaktoroitu haku-listauksen suodattimet ja taulukko erillisiksi reiteiksi --- src/app/(root)/@controls/loading.tsx | 5 + src/app/(root)/@controls/page.tsx | 138 ++++++++++++++++++ src/app/(root)/layout.tsx | 36 +++++ src/app/(root)/loading.tsx | 5 + src/app/(root)/page.tsx | 60 ++++---- src/app/components/haku-filters.tsx | 206 --------------------------- src/app/hooks/useHakuSearch.tsx | 100 +++++++++++-- src/app/hooks/useHakutavat.tsx | 10 ++ src/app/lib/common.ts | 6 +- 9 files changed, 316 insertions(+), 250 deletions(-) create mode 100644 src/app/(root)/@controls/loading.tsx create mode 100644 src/app/(root)/@controls/page.tsx create mode 100644 src/app/(root)/layout.tsx create mode 100644 src/app/(root)/loading.tsx delete mode 100644 src/app/components/haku-filters.tsx create mode 100644 src/app/hooks/useHakutavat.tsx diff --git a/src/app/(root)/@controls/loading.tsx b/src/app/(root)/@controls/loading.tsx new file mode 100644 index 00000000..1277d22f --- /dev/null +++ b/src/app/(root)/@controls/loading.tsx @@ -0,0 +1,5 @@ +import { FullSpinner } from '@/app/components/full-spinner'; + +export default function Loading() { + return ; +} diff --git a/src/app/(root)/@controls/page.tsx b/src/app/(root)/@controls/page.tsx new file mode 100644 index 00000000..b831e9f7 --- /dev/null +++ b/src/app/(root)/@controls/page.tsx @@ -0,0 +1,138 @@ +'use client'; +import React, { ChangeEvent, useMemo } from 'react'; + +import { + FormControl, + Select, + MenuItem, + SelectChangeEvent, + FormLabel, + OutlinedInput, + Checkbox, + FormControlLabel, + Box, + InputAdornment, +} from '@mui/material'; + +import { getHakuAlkamisKaudet } from '../../lib/kouta-types'; +import { Search } from '@mui/icons-material'; +import { useHakuSearchParams } from '../../hooks/useHakuSearch'; +import { useHakutavat } from '@/app/hooks/useHakutavat'; + +export default function HakuControls() { + const alkamiskaudet = useMemo(getHakuAlkamisKaudet, []); + + const { data: hakutavat } = useHakutavat(); + + const { + searchPhrase, + setSearchPhrase, + selectedAlkamisKausi, + setSelectedAlkamisKausi, + selectedHakutapa, + setSelectedHakutapa, + myosArkistoidut, + setMyosArkistoidut, + } = useHakuSearchParams(); + + const handleSearchChange = (e: ChangeEvent) => { + setSearchPhrase(e.target.value); + }; + + const toggleMyosArkistoidut = (_e: unknown, checked: boolean) => { + setMyosArkistoidut(checked); + }; + + const changeHakutapa = (e: SelectChangeEvent) => { + setSelectedHakutapa(e.target.value); + }; + + const changeAlkamisKausi = (e: SelectChangeEvent) => { + setSelectedAlkamisKausi(e.target.value); + }; + + return ( + + + Hae hakuja + + + + } + /> + + } + /> + + + Hakutapa + + + + + Koulutuksen alkamiskausi + + + + + ); +} diff --git a/src/app/(root)/layout.tsx b/src/app/(root)/layout.tsx new file mode 100644 index 00000000..5a2aefe4 --- /dev/null +++ b/src/app/(root)/layout.tsx @@ -0,0 +1,36 @@ +import { CSSProperties } from 'react'; +import Header from '../components/header'; +import { AccessTime as AccessTimeIcon } from '@mui/icons-material'; + +const titleSectionStyle: CSSProperties = { + borderBottom: '1px solid rgba(0, 0, 0, 0.15)', + display: 'flex', + flexDirection: 'row', + width: '100%', + justifyContent: 'flex-start', + marginBottom: '2rem', + padding: 0, +}; + +export default async function HakuLayout({ + children, + controls, +}: { + children: React.ReactNode; + controls: React.ReactNode; +}) { + return ( + <> +
+
+
+

+ Haut +

+
+ {controls} + {children} +
+ + ); +} diff --git a/src/app/(root)/loading.tsx b/src/app/(root)/loading.tsx new file mode 100644 index 00000000..914b08d8 --- /dev/null +++ b/src/app/(root)/loading.tsx @@ -0,0 +1,5 @@ +import { FullSpinner } from '../components/full-spinner'; + +export default function Loading() { + return ; +} diff --git a/src/app/(root)/page.tsx b/src/app/(root)/page.tsx index ab256a86..3235ce0d 100644 --- a/src/app/(root)/page.tsx +++ b/src/app/(root)/page.tsx @@ -1,35 +1,41 @@ -import { getHakutavat } from '../lib/koodisto'; -import { AccessTime as AccessTimeIcon } from '@mui/icons-material'; -import { CSSProperties } from 'react'; -import HakuFilters from '../components/haku-filters'; -import Header from '../components/header'; +'use client'; +import React from 'react'; -const titleSectionStyle: CSSProperties = { - borderBottom: '1px solid rgba(0, 0, 0, 0.15)', - display: 'flex', - flexDirection: 'row', - width: '100%', - justifyContent: 'flex-start', - marginBottom: '2rem', - padding: 0, -}; +import { HakuTable } from '@/app/components/haku-table'; +import { useHakutavat } from '@/app/hooks/useHakutavat'; +import { useHakuSearchResults } from '@/app/hooks/useHakuSearch'; +import { HakuTablePaginationWrapper } from '../components/haku-table-pagination-wrapper'; export const dynamic = 'force-dynamic'; -export default async function Home() { - const hakutavat = await getHakutavat(); +export default function Home() { + const { + page, + setPage, + pageSize, + setPageSize, + results, + pageResults, + sort, + setSort, + } = useHakuSearchResults(); + + const { data: hakutavat } = useHakutavat(); return ( - <> -
-
-
-

- Haut -

-
- -
- + + + ); } diff --git a/src/app/components/haku-filters.tsx b/src/app/components/haku-filters.tsx deleted file mode 100644 index 9cbf36fb..00000000 --- a/src/app/components/haku-filters.tsx +++ /dev/null @@ -1,206 +0,0 @@ -'use client'; -import React, { ChangeEvent, useMemo } from 'react'; - -import { - FormControl, - Select, - MenuItem, - SelectChangeEvent, - FormLabel, - OutlinedInput, - Checkbox, - FormControlLabel, - Box, - InputAdornment, -} from '@mui/material'; - -import { getHakuAlkamisKaudet } from '../lib/kouta-types'; -import { Koodi } from '../lib/koodisto'; -import { HakuTable } from './haku-table'; -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getHaut } from '../lib/kouta'; -import { Search } from '@mui/icons-material'; -import { useHakuSearch } from '../hooks/useHakuSearch'; -import { HakuTablePaginationWrapper } from './haku-table-pagination-wrapper'; - -const KAUSI_MAPPING = Object.freeze({ - kausi_s: { - fi: 'Syksy', - sv: 'Höst', - en: 'Autumn', - }, - kausi_k: { - fi: 'Kevät', - sv: 'Vår', - en: 'Spring', - }, -}); - -const getKausiVuosiTranslation = (kausiUri: string, vuosi: number) => { - if (kausiUri === 'kausi_s' || kausiUri === 'kausi_k') { - const kausiName = KAUSI_MAPPING?.[kausiUri]; - return { - fi: `${vuosi} ${kausiName.fi}`, - sv: `${vuosi} ${kausiName.sv}`, - en: `${vuosi} ${kausiName.en}`, - }; - } -}; - -export const HakuFilters = ({ hakutavat }: { hakutavat: Array }) => { - const { data: haut } = useSuspenseQuery({ - queryKey: ['getHaut'], - queryFn: () => getHaut(), - select: (haut) => - haut.map((haku) => ({ - ...haku, - hakutapaNimi: hakutavat.find( - (hakutapa) => hakutapa.koodiUri === haku.hakutapaKoodiUri, - )?.nimi, - alkamiskausiNimi: getKausiVuosiTranslation( - haku.alkamisKausiKoodiUri?.split('#')?.[0], - haku.alkamisVuosi, - ), - })), - }); - - const alkamiskaudet = useMemo(getHakuAlkamisKaudet, []); - - const { - searchPhrase, - setSearchPhrase, - selectedAlkamisKausi, - setSelectedAlkamisKausi, - selectedHakutapa, - setSelectedHakutapa, - myosArkistoidut, - setMyosArkistoidut, - page, - setPage, - pageSize, - setPageSize, - results, - pageResults, - sort, - setSort, - } = useHakuSearch(haut, alkamiskaudet); - - const handleSearchChange = (e: ChangeEvent) => { - setSearchPhrase(e.target.value); - }; - - const toggleMyosArkistoidut = (_e: unknown, checked: boolean) => { - setMyosArkistoidut(checked); - }; - - const changeHakutapa = (e: SelectChangeEvent) => { - setSelectedHakutapa(e.target.value); - }; - - const changeAlkamisKausi = (e: SelectChangeEvent) => { - setSelectedAlkamisKausi(e.target.value); - }; - - return ( - <> - - - Hae hakuja - - - - } - /> - - } - /> - - - Hakutapa - - - - - Koulutuksen alkamiskausi - - - - - - - - - ); -}; - -export default HakuFilters; diff --git a/src/app/hooks/useHakuSearch.tsx b/src/app/hooks/useHakuSearch.tsx index f00296c9..6e39718d 100644 --- a/src/app/hooks/useHakuSearch.tsx +++ b/src/app/hooks/useHakuSearch.tsx @@ -1,11 +1,19 @@ 'use client'; import { useEffect, useMemo } from 'react'; -import { Haku, HaunAlkaminen, Tila } from '../lib/kouta-types'; +import { + Haku, + HaunAlkaminen, + Tila, + getHakuAlkamisKaudet, +} from '../lib/kouta-types'; import { Language, byProp, getTranslation } from '../lib/common'; import { useDebounce } from '@/app/hooks/useDebounce'; import { parseAsBoolean, parseAsInteger, useQueryState } from 'nuqs'; import { useHasChanged } from '@/app/hooks/useHasChanged'; import { getSortParts } from '../components/table/list-table'; +import { getHaut } from '../lib/kouta'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { useHakutavat } from './useHakutavat'; export const DEFAULT_PAGE_SIZE = 30; @@ -25,10 +33,31 @@ export const alkamisKausiMatchesSelected = ( selectedAlkamisKausi.alkamisKausiKoodiUri, )); -export const useHakuSearch = ( - haut: Array, - alkamiskaudet: Array, -) => { +const KAUSI_MAPPING = Object.freeze({ + kausi_s: { + fi: 'Syksy', + sv: 'Höst', + en: 'Autumn', + }, + kausi_k: { + fi: 'Kevät', + sv: 'Vår', + en: 'Spring', + }, +}); + +const getKausiVuosiTranslation = (kausiUri: string, vuosi: number) => { + if (kausiUri === 'kausi_s' || kausiUri === 'kausi_k') { + const kausiName = KAUSI_MAPPING?.[kausiUri]; + return { + fi: `${vuosi} ${kausiName.fi}`, + sv: `${vuosi} ${kausiName.sv}`, + en: `${vuosi} ${kausiName.en}`, + }; + } +}; + +export const useHakuSearchParams = () => { const [searchPhrase, setSearchPhrase] = useQueryState( 'search', DEFAULT_NUQS_OPTIONS, @@ -87,12 +116,63 @@ export const useHakuSearch = ( setPage, ]); + return { + searchPhrase, + setSearchPhrase: setSearchDebounce, + myosArkistoidut, + setMyosArkistoidut: setMyosArkistoidut, + selectedHakutapa, + setSelectedHakutapa, + selectedAlkamisKausi, + setSelectedAlkamisKausi, + page, + setPage, + pageSize, + setPageSize, + sort, + setSort, + }; +}; + +export const useHakuSearchResults = () => { + const alkamiskaudet = useMemo(getHakuAlkamisKaudet, []); + const { data: hakutavat } = useHakutavat(); + + const { data: haut } = useSuspenseQuery({ + queryKey: ['getHaut'], + queryFn: () => getHaut(), + select: (haut) => + haut.map((haku) => ({ + ...haku, + hakutapaNimi: hakutavat.find( + (hakutapa) => hakutapa.koodiUri === haku.hakutapaKoodiUri, + )?.nimi, + alkamiskausiNimi: getKausiVuosiTranslation( + haku.alkamisKausiKoodiUri?.split('#')?.[0], + haku.alkamisVuosi, + ), + })), + }); + + const { + searchPhrase, + myosArkistoidut, + selectedHakutapa, + selectedAlkamisKausi, + page, + setPage, + pageSize, + setPageSize, + sort, + setSort, + } = useHakuSearchParams(); + const results = useMemo(() => { const tilat = myosArkistoidut ? [Tila.JULKAISTU, Tila.ARKISTOITU] : [Tila.JULKAISTU]; - const { orderBy, direction } = getSortParts(sort ?? ''); + const { orderBy, direction } = getSortParts(sort); const filtered = haut.filter( (haku: Haku) => @@ -125,14 +205,6 @@ export const useHakuSearch = ( }, [results, page, pageSize]); return { - searchPhrase, - setSearchPhrase: setSearchDebounce, - myosArkistoidut, - setMyosArkistoidut: setMyosArkistoidut, - selectedHakutapa, - setSelectedHakutapa, - selectedAlkamisKausi, - setSelectedAlkamisKausi, page, setPage, pageSize, diff --git a/src/app/hooks/useHakutavat.tsx b/src/app/hooks/useHakutavat.tsx new file mode 100644 index 00000000..5a74a6a0 --- /dev/null +++ b/src/app/hooks/useHakutavat.tsx @@ -0,0 +1,10 @@ +'use client'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { getHakutavat } from '../lib/koodisto'; + +export const useHakutavat = () => + useSuspenseQuery({ + queryKey: ['getHakutavat'], + queryFn: () => getHakutavat(), + staleTime: Infinity, + }); diff --git a/src/app/lib/common.ts b/src/app/lib/common.ts index cd155708..0839b19d 100644 --- a/src/app/lib/common.ts +++ b/src/app/lib/common.ts @@ -51,7 +51,7 @@ function isTranslatedName(value: unknown): value is TranslatedName { } export const byProp = < - T extends Record, + T extends Record, >( key: string, direction: 'asc' | 'desc' = 'asc', @@ -59,10 +59,10 @@ export const byProp = < ) => { const asc = direction === 'asc'; return (a: T, b: T) => { - const aKey = a[key]; + const aKey = a[key] ?? ''; const aProp = isTranslatedName(aKey) ? getTranslation(aKey, lng) : aKey; - const bKey = b[key]; + const bKey = b[key] ?? ''; const bProp = isTranslatedName(bKey) ? getTranslation(bKey, lng) : bKey; return aProp > bProp ? (asc ? 1 : -1) : bProp > aProp ? (asc ? -1 : 1) : 0; From eec6a7bf4d15cc6a55bff1f26331c7bc8ad729cb Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 13:41:47 +0300 Subject: [PATCH 26/36] =?UTF-8?q?Pient=C3=A4=20refaktorointia=20ja=20koodi?= =?UTF-8?q?n=20siivousta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(root)/layout.tsx | 2 +- .../haku-table-pagination-wrapper.tsx | 4 +--- src/app/components/table/getSortParts.tsx | 17 +++++++++++++++++ src/app/components/table/list-table.tsx | 19 +------------------ src/app/hooks/useHakuSearch.tsx | 13 ++++++++----- src/app/lib/common.ts | 4 ++++ 6 files changed, 32 insertions(+), 27 deletions(-) create mode 100644 src/app/components/table/getSortParts.tsx diff --git a/src/app/(root)/layout.tsx b/src/app/(root)/layout.tsx index 5a2aefe4..997e2036 100644 --- a/src/app/(root)/layout.tsx +++ b/src/app/(root)/layout.tsx @@ -12,7 +12,7 @@ const titleSectionStyle: CSSProperties = { padding: 0, }; -export default async function HakuLayout({ +export default async function HakuListLayout({ children, controls, }: { diff --git a/src/app/components/haku-table-pagination-wrapper.tsx b/src/app/components/haku-table-pagination-wrapper.tsx index 577bbf21..8cc7c844 100644 --- a/src/app/components/haku-table-pagination-wrapper.tsx +++ b/src/app/components/haku-table-pagination-wrapper.tsx @@ -10,9 +10,7 @@ import { styled, Pagination, } from '@mui/material'; -import { DEFAULT_PAGE_SIZE } from '../hooks/useHakuSearch'; - -export const PAGE_SIZES = [10, 20, 30, 50, 100]; +import { DEFAULT_PAGE_SIZE, PAGE_SIZES } from '@/app/lib/common'; export const StyledPagination = styled(Pagination)({ display: 'flex', diff --git a/src/app/components/table/getSortParts.tsx b/src/app/components/table/getSortParts.tsx new file mode 100644 index 00000000..bed04e5c --- /dev/null +++ b/src/app/components/table/getSortParts.tsx @@ -0,0 +1,17 @@ +export const getSortParts = (sortStr: string, colId?: string) => { + const [orderBy, direction] = sortStr?.split(':') ?? ''; + + if ( + (colId === undefined || colId === orderBy) && + (direction === 'asc' || direction === 'desc') + ) { + return { orderBy, direction } as { + orderBy: string; + direction: 'asc' | 'desc'; + }; + } + return { + orderBy: undefined, + direction: undefined, + }; +}; diff --git a/src/app/components/table/list-table.tsx b/src/app/components/table/list-table.tsx index a89e8cb0..6b0878b9 100644 --- a/src/app/components/table/list-table.tsx +++ b/src/app/components/table/list-table.tsx @@ -14,6 +14,7 @@ import { TranslatedName, getTranslation } from '@/app/lib/common'; import { Haku, getAlkamisKausi, Tila } from '@/app/lib/kouta-types'; import { colors } from '@/app/theme'; import { ExpandLess, ExpandMore, UnfoldMore } from '@mui/icons-material'; +import { getSortParts } from './getSortParts'; type Column

= { title?: string; @@ -133,24 +134,6 @@ const SortIcon = ({ } }; -export const getSortParts = (sortStr: string, colId?: string) => { - const [orderBy, direction] = sortStr?.split(':') ?? ''; - - if ( - (colId === undefined || colId === orderBy) && - (direction === 'asc' || direction === 'desc') - ) { - return { orderBy, direction } as { - orderBy: string; - direction: 'asc' | 'desc'; - }; - } - return { - orderBy: undefined, - direction: undefined, - }; -}; - const HeaderCell = ({ colId, title, diff --git a/src/app/hooks/useHakuSearch.tsx b/src/app/hooks/useHakuSearch.tsx index 6e39718d..88d2959a 100644 --- a/src/app/hooks/useHakuSearch.tsx +++ b/src/app/hooks/useHakuSearch.tsx @@ -6,24 +6,27 @@ import { Tila, getHakuAlkamisKaudet, } from '../lib/kouta-types'; -import { Language, byProp, getTranslation } from '../lib/common'; +import { + DEFAULT_PAGE_SIZE, + Language, + byProp, + getTranslation, +} from '../lib/common'; import { useDebounce } from '@/app/hooks/useDebounce'; import { parseAsBoolean, parseAsInteger, useQueryState } from 'nuqs'; import { useHasChanged } from '@/app/hooks/useHasChanged'; -import { getSortParts } from '../components/table/list-table'; +import { getSortParts } from '../components/table/getSortParts'; import { getHaut } from '../lib/kouta'; import { useSuspenseQuery } from '@tanstack/react-query'; import { useHakutavat } from './useHakutavat'; -export const DEFAULT_PAGE_SIZE = 30; - const DEFAULT_NUQS_OPTIONS = { history: 'push', clearOnDefault: true, defaultValue: '', } as const; -export const alkamisKausiMatchesSelected = ( +const alkamisKausiMatchesSelected = ( haku: Haku, selectedAlkamisKausi?: HaunAlkaminen, ): boolean => diff --git a/src/app/lib/common.ts b/src/app/lib/common.ts index 0839b19d..57954b12 100644 --- a/src/app/lib/common.ts +++ b/src/app/lib/common.ts @@ -68,3 +68,7 @@ export const byProp = < return aProp > bProp ? (asc ? 1 : -1) : bProp > aProp ? (asc ? -1 : 1) : 0; }; }; + +export const PAGE_SIZES = [10, 20, 30, 50, 100]; + +export const DEFAULT_PAGE_SIZE = 30; From a6eab6d5a1d3afc9e89fde8b2abdac4ee87cef67 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 14:40:15 +0300 Subject: [PATCH 27/36] Odotetaan ensin URL:n muutosta expectUrlParamToEqual-funktiossa --- tests/e2e/playwright-utils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/e2e/playwright-utils.ts b/tests/e2e/playwright-utils.ts index 907508b8..c4bddeba 100644 --- a/tests/e2e/playwright-utils.ts +++ b/tests/e2e/playwright-utils.ts @@ -16,6 +16,11 @@ export const expectUrlParamToEqual = async ( paramName: string, value: string, ) => { + await page.waitForURL( + new RegExp( + `(&|\\?)${paramName}=${value}|${encodeURIComponent(value)}(&|$)`, + ), + ); const pageURL = await page.url(); const urlObj = new URL(pageURL); const param = urlObj.searchParams.get(paramName); From db7b4688de9e48e0d43dc7d1fc0991ba5b6002b1 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Mon, 13 May 2024 14:43:44 +0300 Subject: [PATCH 28/36] =?UTF-8?q?Nostetaan=20Playwrightin=20workereiden=20?= =?UTF-8?q?m=C3=A4=C3=A4r=C3=A4=20kahteen=20CI:ss=C3=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.ts b/playwright.config.ts index 50fda12c..23ca8d2a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 2 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'list', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ From 86c92f3f28fe464778e121b3626e59bbbfc3002f Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Tue, 14 May 2024 13:09:27 +0300 Subject: [PATCH 29/36] =?UTF-8?q?OK-508:=20Vaihdettu=20haku-listauksen=20"?= =?UTF-8?q?My=C3=B6s=20arkistoidut"-checkbox=20->=20"Tila"-dropdowniksi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(root)/@controls/page.tsx | 98 ++++++++++++++++++------------- src/app/hooks/useHakuSearch.tsx | 36 +++++++----- tests/e2e/haut.spec.ts | 32 +++++++--- 3 files changed, 101 insertions(+), 65 deletions(-) diff --git a/src/app/(root)/@controls/page.tsx b/src/app/(root)/@controls/page.tsx index b831e9f7..ae22297e 100644 --- a/src/app/(root)/@controls/page.tsx +++ b/src/app/(root)/@controls/page.tsx @@ -8,22 +8,20 @@ import { SelectChangeEvent, FormLabel, OutlinedInput, - Checkbox, - FormControlLabel, Box, InputAdornment, } from '@mui/material'; -import { getHakuAlkamisKaudet } from '../../lib/kouta-types'; +import { Tila, getHakuAlkamisKaudet } from '../../lib/kouta-types'; import { Search } from '@mui/icons-material'; import { useHakuSearchParams } from '../../hooks/useHakuSearch'; import { useHakutavat } from '@/app/hooks/useHakutavat'; export default function HakuControls() { - const alkamiskaudet = useMemo(getHakuAlkamisKaudet, []); - const { data: hakutavat } = useHakutavat(); + const alkamiskaudet = useMemo(getHakuAlkamisKaudet, []); + const { searchPhrase, setSearchPhrase, @@ -31,16 +29,16 @@ export default function HakuControls() { setSelectedAlkamisKausi, selectedHakutapa, setSelectedHakutapa, - myosArkistoidut, - setMyosArkistoidut, + tila, + setTila, } = useHakuSearchParams(); const handleSearchChange = (e: ChangeEvent) => { setSearchPhrase(e.target.value); }; - const toggleMyosArkistoidut = (_e: unknown, checked: boolean) => { - setMyosArkistoidut(checked); + const changeTila = (e: SelectChangeEvent) => { + setTila(e.target.value); }; const changeHakutapa = (e: SelectChangeEvent) => { @@ -59,39 +57,57 @@ export default function HakuControls() { gap={2} flexWrap="wrap" > - - Hae hakuja - - - - } - /> - - } - /> - + + Hae hakuja + + + + } + /> + + + Tila + + + Hakutapa + Valitse... + {hakutavat.map((tapa) => { + return ( + + {tapa.nimi.fi} + + ); //TODO: translate + })} + + ); +}; + +const SelectFallback = () => ( + - - Hakutapa - - + Koulutuksen alkamiskausi From db42156a1cfd21af971a103a4fd8e3a5592b94f8 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Tue, 14 May 2024 17:17:15 +0300 Subject: [PATCH 31/36] Korjattu NextJs standalone-moodin asettaminen --- next.config.mjs | 4 +--- package.json | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index 82c946c2..97fc237d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -14,9 +14,7 @@ const cspHeader = ` upgrade-insecure-requests; `; -const isProd = process.env.NODE_ENV === 'production'; - -const isStandalone = process.env.NEXT_OUTPUT_STANDALONE === 'true' ?? isProd; +const isStandalone = process.env.STANDALONE === 'true'; const basePath = '/valintojen-toteuttaminen'; diff --git a/package.json b/package.json index 274e5033..38553d03 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "scripts": { "dev": "touch .env.development.local && NODE_EXTRA_CA_CERTS=\"$(mkcert -CAROOT)/rootCA.pem\" node --env-file=.env --env-file=.env.development.local dev-server.mjs", "dev-test": "TEST=true APP_URL=https://localhost:3404 VIRKAILIJA_URL=http://localhost:3104 npm run dev", - "build": "next build; cp run.sh .next/standalone", - "build-test": "APP_URL=https://localhost:3404 VIRKAILIJA_URL=http://localhost:3104 NEXT_OUTPUT_STANDALONE=false next build", + "build": "STANDALONE=true next build; cp run.sh .next/standalone", + "build-test": "APP_URL=https://localhost:3404 VIRKAILIJA_URL=http://localhost:3104 next build", "start": "NODE_ENV=production npm run dev", "start-test": "NODE_ENV=production npm run dev-test", "lint": "next lint", From 8b7427f2cb15e1d722924014c1ae3ccc34b7d81e Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Wed, 15 May 2024 09:57:55 +0300 Subject: [PATCH 32/36] =?UTF-8?q?Siirretty=20yksikk=C3=B6testit=20samaan?= =?UTF-8?q?=20hakemistoon=20kuin=20testattava=20tiedosto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/table/getSortParts.tsx | 17 ----------------- {tests/unit => src/app/lib}/common.test.ts | 6 +----- vitest.config.ts | 3 ++- 3 files changed, 3 insertions(+), 23 deletions(-) delete mode 100644 src/app/components/table/getSortParts.tsx rename {tests/unit => src/app/lib}/common.test.ts (90%) diff --git a/src/app/components/table/getSortParts.tsx b/src/app/components/table/getSortParts.tsx deleted file mode 100644 index bed04e5c..00000000 --- a/src/app/components/table/getSortParts.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export const getSortParts = (sortStr: string, colId?: string) => { - const [orderBy, direction] = sortStr?.split(':') ?? ''; - - if ( - (colId === undefined || colId === orderBy) && - (direction === 'asc' || direction === 'desc') - ) { - return { orderBy, direction } as { - orderBy: string; - direction: 'asc' | 'desc'; - }; - } - return { - orderBy: undefined, - direction: undefined, - }; -}; diff --git a/tests/unit/common.test.ts b/src/app/lib/common.test.ts similarity index 90% rename from tests/unit/common.test.ts rename to src/app/lib/common.test.ts index 7b84be6c..53751391 100644 --- a/tests/unit/common.test.ts +++ b/src/app/lib/common.test.ts @@ -1,9 +1,5 @@ import { expect, test } from 'vitest'; -import { - getTranslation, - Language, - TranslatedName, -} from '../../src/app/lib/common'; +import { getTranslation, Language, TranslatedName } from './common'; const school: TranslatedName = { fi: 'koulu', en: 'a school', sv: 'en skola' }; diff --git a/vitest.config.ts b/vitest.config.ts index 0fdc922c..caf7ea3a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,7 +5,8 @@ export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', - dir: './tests/unit', + dir: './src', + include: ['**/**.test.?(c|m)[jt]s?(x)'], coverage: { provider: 'istanbul', include: ['src/**'], From cabd7c420e46bd59ea8385252d2425afa37eba8b Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Wed, 15 May 2024 09:58:20 +0300 Subject: [PATCH 33/36] =?UTF-8?q?OK-508:=20Lis=C3=A4tty=20yksikk=C3=B6test?= =?UTF-8?q?it=20getSortParts-funktiolle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/table/getSortParts.test.ts | 24 +++++++++++++++++++ src/app/components/table/getSortParts.ts | 17 +++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/app/components/table/getSortParts.test.ts create mode 100644 src/app/components/table/getSortParts.ts diff --git a/src/app/components/table/getSortParts.test.ts b/src/app/components/table/getSortParts.test.ts new file mode 100644 index 00000000..266aac59 --- /dev/null +++ b/src/app/components/table/getSortParts.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from 'vitest'; +import { getSortParts } from './getSortParts'; + +const EMPTY_RESULT = { + orderBy: undefined, + direction: undefined, +}; + +test('getSortParts', () => { + expect(getSortParts('col:asc', 'col')).toEqual({ + orderBy: 'col', + direction: 'asc', + }); + expect(getSortParts('col:asc')).toEqual({ + orderBy: 'col', + direction: 'asc', + }); + expect(getSortParts('col:desc', 'col')).toEqual({ + orderBy: 'col', + direction: 'desc', + }); + expect(getSortParts('', 'col')).toEqual(EMPTY_RESULT); + expect(getSortParts(undefined, 'col')).toEqual(EMPTY_RESULT); +}); diff --git a/src/app/components/table/getSortParts.ts b/src/app/components/table/getSortParts.ts new file mode 100644 index 00000000..f3a32911 --- /dev/null +++ b/src/app/components/table/getSortParts.ts @@ -0,0 +1,17 @@ +export const getSortParts = (sortStr?: string, colId?: string) => { + const [orderBy, direction] = sortStr?.split(':') ?? []; + + if ( + (colId === undefined || colId === orderBy) && + (direction === 'asc' || direction === 'desc') + ) { + return { orderBy, direction } as { + orderBy: string; + direction: 'asc' | 'desc'; + }; + } + return { + orderBy: undefined, + direction: undefined, + }; +}; From e72afc880c008a043adee7160ec83e7907f10ae0 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Wed, 15 May 2024 10:34:22 +0300 Subject: [PATCH 34/36] =?UTF-8?q?Lis=C3=A4tty=20path=20alias=20vitest:lle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vitest.config.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vitest.config.ts b/vitest.config.ts index caf7ea3a..1fd3f183 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,14 @@ +import path from 'path'; import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, test: { environment: 'jsdom', dir: './src', From 7e21e2e22cc3ed468f7002dc517b9144991976f7 Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Wed, 15 May 2024 10:50:42 +0300 Subject: [PATCH 35/36] =?UTF-8?q?Refaktoroitu=20koodia=20parempaan=20j?= =?UTF-8?q?=C3=A4rjestykseen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../haku-table-pagination-wrapper.tsx | 2 +- src/app/{components => (root)}/haku-table.tsx | 2 +- src/app/(root)/page.tsx | 4 +- src/app/components/table/getSortParts.ts | 17 -------- src/app/components/table/list-table.tsx | 2 +- ...tSortParts.test.ts => table-utils.test.ts} | 2 +- src/app/components/table/table-utils.ts | 43 +++++++++++++++++++ src/app/hooks/useHakuSearch.tsx | 18 ++++---- src/app/lib/common.ts | 27 +----------- src/app/lib/constants.ts | 5 +++ 10 files changed, 66 insertions(+), 56 deletions(-) rename src/app/{components => (root)}/haku-table-pagination-wrapper.tsx (96%) rename src/app/{components => (root)}/haku-table.tsx (95%) delete mode 100644 src/app/components/table/getSortParts.ts rename src/app/components/table/{getSortParts.test.ts => table-utils.test.ts} (92%) create mode 100644 src/app/components/table/table-utils.ts create mode 100644 src/app/lib/constants.ts diff --git a/src/app/components/haku-table-pagination-wrapper.tsx b/src/app/(root)/haku-table-pagination-wrapper.tsx similarity index 96% rename from src/app/components/haku-table-pagination-wrapper.tsx rename to src/app/(root)/haku-table-pagination-wrapper.tsx index 8cc7c844..5e9a0784 100644 --- a/src/app/components/haku-table-pagination-wrapper.tsx +++ b/src/app/(root)/haku-table-pagination-wrapper.tsx @@ -10,7 +10,7 @@ import { styled, Pagination, } from '@mui/material'; -import { DEFAULT_PAGE_SIZE, PAGE_SIZES } from '@/app/lib/common'; +import { DEFAULT_PAGE_SIZE, PAGE_SIZES } from '@/app/lib/constants'; export const StyledPagination = styled(Pagination)({ display: 'flex', diff --git a/src/app/components/haku-table.tsx b/src/app/(root)/haku-table.tsx similarity index 95% rename from src/app/components/haku-table.tsx rename to src/app/(root)/haku-table.tsx index 320a34b4..84fb0393 100644 --- a/src/app/components/haku-table.tsx +++ b/src/app/(root)/haku-table.tsx @@ -6,7 +6,7 @@ import ListTable, { makeHakutapaColumn, makeKoulutuksenAlkamiskausiColumn, makeTilaColumn, -} from './table/list-table'; +} from '../components/table/list-table'; import { Haku } from '../lib/kouta-types'; export const HakuTable = ({ diff --git a/src/app/(root)/page.tsx b/src/app/(root)/page.tsx index 3235ce0d..93709949 100644 --- a/src/app/(root)/page.tsx +++ b/src/app/(root)/page.tsx @@ -1,10 +1,10 @@ 'use client'; import React from 'react'; -import { HakuTable } from '@/app/components/haku-table'; import { useHakutavat } from '@/app/hooks/useHakutavat'; import { useHakuSearchResults } from '@/app/hooks/useHakuSearch'; -import { HakuTablePaginationWrapper } from '../components/haku-table-pagination-wrapper'; +import { HakuTablePaginationWrapper } from './haku-table-pagination-wrapper'; +import { HakuTable } from './haku-table'; export const dynamic = 'force-dynamic'; diff --git a/src/app/components/table/getSortParts.ts b/src/app/components/table/getSortParts.ts deleted file mode 100644 index f3a32911..00000000 --- a/src/app/components/table/getSortParts.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const getSortParts = (sortStr?: string, colId?: string) => { - const [orderBy, direction] = sortStr?.split(':') ?? []; - - if ( - (colId === undefined || colId === orderBy) && - (direction === 'asc' || direction === 'desc') - ) { - return { orderBy, direction } as { - orderBy: string; - direction: 'asc' | 'desc'; - }; - } - return { - orderBy: undefined, - direction: undefined, - }; -}; diff --git a/src/app/components/table/list-table.tsx b/src/app/components/table/list-table.tsx index 6b0878b9..98fbcb55 100644 --- a/src/app/components/table/list-table.tsx +++ b/src/app/components/table/list-table.tsx @@ -14,7 +14,7 @@ import { TranslatedName, getTranslation } from '@/app/lib/common'; import { Haku, getAlkamisKausi, Tila } from '@/app/lib/kouta-types'; import { colors } from '@/app/theme'; import { ExpandLess, ExpandMore, UnfoldMore } from '@mui/icons-material'; -import { getSortParts } from './getSortParts'; +import { getSortParts } from './table-utils'; type Column

= { title?: string; diff --git a/src/app/components/table/getSortParts.test.ts b/src/app/components/table/table-utils.test.ts similarity index 92% rename from src/app/components/table/getSortParts.test.ts rename to src/app/components/table/table-utils.test.ts index 266aac59..b415e775 100644 --- a/src/app/components/table/getSortParts.test.ts +++ b/src/app/components/table/table-utils.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import { getSortParts } from './getSortParts'; +import { getSortParts } from './table-utils'; const EMPTY_RESULT = { orderBy: undefined, diff --git a/src/app/components/table/table-utils.ts b/src/app/components/table/table-utils.ts new file mode 100644 index 00000000..7e433567 --- /dev/null +++ b/src/app/components/table/table-utils.ts @@ -0,0 +1,43 @@ +import { + Language, + TranslatedName, + getTranslation, + isTranslatedName, +} from '@/app/lib/common'; + +export const byProp = < + T extends Record, +>( + key: string, + direction: 'asc' | 'desc' = 'asc', + lng: Language, +) => { + const asc = direction === 'asc'; + return (a: T, b: T) => { + const aKey = a[key] ?? ''; + const aProp = isTranslatedName(aKey) ? getTranslation(aKey, lng) : aKey; + + const bKey = b[key] ?? ''; + const bProp = isTranslatedName(bKey) ? getTranslation(bKey, lng) : bKey; + + return aProp > bProp ? (asc ? 1 : -1) : bProp > aProp ? (asc ? -1 : 1) : 0; + }; +}; + +export const getSortParts = (sortStr?: string, colId?: string) => { + const [orderBy, direction] = sortStr?.split(':') ?? []; + + if ( + (colId === undefined || colId === orderBy) && + (direction === 'asc' || direction === 'desc') + ) { + return { orderBy, direction } as { + orderBy: string; + direction: 'asc' | 'desc'; + }; + } + return { + orderBy: undefined, + direction: undefined, + }; +}; diff --git a/src/app/hooks/useHakuSearch.tsx b/src/app/hooks/useHakuSearch.tsx index b4d3e40c..4aa1c227 100644 --- a/src/app/hooks/useHakuSearch.tsx +++ b/src/app/hooks/useHakuSearch.tsx @@ -6,19 +6,18 @@ import { Tila, getHakuAlkamisKaudet, } from '../lib/kouta-types'; -import { - DEFAULT_PAGE_SIZE, - Language, - byProp, - getTranslation, -} from '../lib/common'; +import { Language, getTranslation } from '../lib/common'; import { useDebounce } from '@/app/hooks/useDebounce'; import { parseAsInteger, useQueryState } from 'nuqs'; import { useHasChanged } from '@/app/hooks/useHasChanged'; -import { getSortParts } from '../components/table/getSortParts'; +import { byProp, getSortParts } from '../components/table/table-utils'; import { getHaut } from '../lib/kouta'; import { useSuspenseQuery } from '@tanstack/react-query'; import { useHakutavat } from './useHakutavat'; +import { + DEFAULT_PAGE_SIZE, + HAKU_SEARCH_PHRASE_DEBOUNCE_DELAY, +} from '@/app/lib/constants'; const DEFAULT_NUQS_OPTIONS = { history: 'push', @@ -66,7 +65,10 @@ export const useHakuSearchParams = () => { DEFAULT_NUQS_OPTIONS, ); - const setSearchDebounce = useDebounce(setSearchPhrase, 500); + const setSearchDebounce = useDebounce( + setSearchPhrase, + HAKU_SEARCH_PHRASE_DEBOUNCE_DELAY, + ); const [tila, setTila] = useQueryState('tila', { history: 'push', diff --git a/src/app/lib/common.ts b/src/app/lib/common.ts index 57954b12..ba8626b5 100644 --- a/src/app/lib/common.ts +++ b/src/app/lib/common.ts @@ -37,11 +37,11 @@ export class FetchError extends Error { } } -function isObject(value: unknown): value is Record { +export function isObject(value: unknown): value is Record { return typeof value === 'object' && value !== null; } -function isTranslatedName(value: unknown): value is TranslatedName { +export function isTranslatedName(value: unknown): value is TranslatedName { return ( isObject(value) && (typeof value?.fi === 'string' || @@ -49,26 +49,3 @@ function isTranslatedName(value: unknown): value is TranslatedName { typeof value?.en === 'string') ); } - -export const byProp = < - T extends Record, ->( - key: string, - direction: 'asc' | 'desc' = 'asc', - lng: Language, -) => { - const asc = direction === 'asc'; - return (a: T, b: T) => { - const aKey = a[key] ?? ''; - const aProp = isTranslatedName(aKey) ? getTranslation(aKey, lng) : aKey; - - const bKey = b[key] ?? ''; - const bProp = isTranslatedName(bKey) ? getTranslation(bKey, lng) : bKey; - - return aProp > bProp ? (asc ? 1 : -1) : bProp > aProp ? (asc ? -1 : 1) : 0; - }; -}; - -export const PAGE_SIZES = [10, 20, 30, 50, 100]; - -export const DEFAULT_PAGE_SIZE = 30; diff --git a/src/app/lib/constants.ts b/src/app/lib/constants.ts new file mode 100644 index 00000000..8a8a1eab --- /dev/null +++ b/src/app/lib/constants.ts @@ -0,0 +1,5 @@ +export const PAGE_SIZES = [10, 20, 30, 50, 100]; + +export const DEFAULT_PAGE_SIZE = 30; + +export const HAKU_SEARCH_PHRASE_DEBOUNCE_DELAY = 500; From b7b4c29481893791dc9f0558fcb745d314faf82c Mon Sep 17 00:00:00 2001 From: Petteri Tolonen Date: Wed, 15 May 2024 13:59:38 +0300 Subject: [PATCH 36/36] Parannettu haun suodatin-kenttien asettelua --- src/app/(root)/@controls/page.tsx | 135 ++++++++++++++++-------------- 1 file changed, 71 insertions(+), 64 deletions(-) diff --git a/src/app/(root)/@controls/page.tsx b/src/app/(root)/@controls/page.tsx index 331dfca4..20699216 100644 --- a/src/app/(root)/@controls/page.tsx +++ b/src/app/(root)/@controls/page.tsx @@ -13,9 +13,9 @@ import { CircularProgress, } from '@mui/material'; -import { Tila, getHakuAlkamisKaudet } from '../../lib/kouta-types'; +import { Tila, getHakuAlkamisKaudet } from '@/app/lib/kouta-types'; import { Search } from '@mui/icons-material'; -import { useHakuSearchParams } from '../../hooks/useHakuSearch'; +import { useHakuSearchParams } from '@/app/hooks/useHakuSearch'; import { useHakutavat } from '@/app/hooks/useHakutavat'; const HakutapaSelect = ({ @@ -67,7 +67,7 @@ const HakutapaInput = ({ onChange: (e: SelectChangeEvent) => void; }) => { return ( - + Hakutapa }> @@ -110,84 +110,91 @@ export default function HakuControls() { + + Hae hakuja + + + + } + /> + + + Tila + + - - Hae hakuja - - - - } - /> - - - Tila + + + + Koulutuksen alkamiskausi + - - - - Koulutuksen alkamiskausi - - - ); }