From 80179c99b2239bdf2346ed7c47d5a607ea2aa0b6 Mon Sep 17 00:00:00 2001 From: Marja Kari Date: Tue, 19 Nov 2024 10:31:19 +0200 Subject: [PATCH] OK-683 & OK-713 (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * OK-683 nollataan lähetysten sivutus jos hakukriteeri muuttuu * Mui v6 TreeSelectin migraatio * OK-713 korjattu organisaatiovalinta ja päivitetty käyttämään uusia mui-komponentteja * OK-713 passataan kieli parametrina eikä hookin kautta organsaatiopuulle, parametrin uudelleennimentä ja sivutuksen nollaus organisaation vaihtuessa * OK-683 nollataan sivutus myös vastaanottajalistalla jos hakukriteeri muuttuu --- .../resource/LahetysResource.scala | 1 - viestinvalitys-raportointi/src/app/Haku.tsx | 115 +++++++++----- .../src/app/components/OrganisaatioFilter.tsx | 2 +- .../app/components/OrganisaatioHierarkia.tsx | 109 +++++--------- .../src/app/components/OrganisaatioSelect.tsx | 141 ++++++++++-------- .../src/app/hooks/useHasChanged.ts | 10 ++ .../src/app/hooks/usePrevious.ts | 9 ++ .../lahetys/[tunniste]/VastaanottajaHaku.tsx | 32 +++- .../src/app/lib/data.ts | 6 +- .../src/app/lib/searchParams.ts | 2 +- 10 files changed, 239 insertions(+), 188 deletions(-) create mode 100644 viestinvalitys-raportointi/src/app/hooks/useHasChanged.ts create mode 100644 viestinvalitys-raportointi/src/app/hooks/usePrevious.ts diff --git a/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/resource/LahetysResource.scala b/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/resource/LahetysResource.scala index deb8aeba..36cee38a 100644 --- a/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/resource/LahetysResource.scala +++ b/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/resource/LahetysResource.scala @@ -431,7 +431,6 @@ class LahetysResource { try // suodatetaan pois swagger-esimerkkirivin palvelu val palvelut = kantaOperaatiot.getLahettavatPalvelut().filterNot(p => p.equals("Esimerkkipalvelu")) - LOG.info(s"Löytyi ${palvelut.size} palvelua") ResponseEntity.status(HttpStatus.OK).body(write[List[String]](palvelut)) catch case e: Exception => diff --git a/viestinvalitys-raportointi/src/app/Haku.tsx b/viestinvalitys-raportointi/src/app/Haku.tsx index 6dd996b4..7381aeeb 100644 --- a/viestinvalitys-raportointi/src/app/Haku.tsx +++ b/viestinvalitys-raportointi/src/app/Haku.tsx @@ -7,7 +7,10 @@ import { SelectChangeEvent, } from '@mui/material'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; -import { LocalizationProvider, PickersActionBarProps } from '@mui/x-date-pickers'; +import { + LocalizationProvider, + PickersActionBarProps, +} from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import 'dayjs/locale/fi'; import 'dayjs/locale/sv'; @@ -19,20 +22,29 @@ import { Search } from '@mui/icons-material'; import { NUQS_DEFAULT_OPTIONS } from './lib/constants'; import { LahettavaPalveluInput } from './components/LahettavaPalveluInput'; import { OphFormControl } from './components/OphFormControl'; -import { OphButton, ophColors, OphSelect } from '@opetushallitus/oph-design-system'; +import { + OphButton, + ophColors, + OphSelect, +} from '@opetushallitus/oph-design-system'; import dayjs from 'dayjs'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { useHasChanged } from './hooks/useHasChanged'; function CustomActionBar(props: PickersActionBarProps) { const { onAccept, onClear, className } = props; const t = useTranslations(); return ( - - {t('yleinen.ok')} - {t('yleinen.tyhjenna')} - + + + {t('yleinen.ok')} + + + {t('yleinen.tyhjenna')} + + ); - } +} const HakukenttaSelect = ({ labelId, @@ -96,23 +108,56 @@ export default function Haku({ 'hakusana', NUQS_DEFAULT_OPTIONS, ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [seuraavatAlkaen, setSeuraavatAlkaen] = useQueryState( + 'seuraavatAlkaen', + NUQS_DEFAULT_OPTIONS, + ); const [palvelu, setPalvelu] = useQueryState('palvelu', NUQS_DEFAULT_OPTIONS); - const [hakuAlkaen, setHakuAlkaen] = useQueryState('hakuAlkaen', NUQS_DEFAULT_OPTIONS); - const [hakuPaattyen, setHakuPaattyen] = useQueryState('hakuPaattyen', NUQS_DEFAULT_OPTIONS); - const [calendarErrors, setCalendarErrors] = useState>([]) + const [hakuAlkaen, setHakuAlkaen] = useQueryState( + 'hakuAlkaen', + NUQS_DEFAULT_OPTIONS, + ); + const [hakuPaattyen, setHakuPaattyen] = useQueryState( + 'hakuPaattyen', + NUQS_DEFAULT_OPTIONS, + ); + const [calendarErrors, setCalendarErrors] = useState>([]); -const handleAlkuDateTimeChange = (value: dayjs.Dayjs | null) => { - setHakuAlkaen(value?.toISOString() ?? null) -}; + const hakusanaChanged = useHasChanged(hakusana); + const palveluChanged = useHasChanged(palvelu); + const hakuAlkaenChanged = useHasChanged(hakuAlkaen); + const hakuPaattyenChanged = useHasChanged(hakuPaattyen); -const handleLoppuDateTimeChange = (value: dayjs.Dayjs | null) => { - if(hakuAlkaen && value && !dayjs(hakuAlkaen).isBefore(value)) { - setCalendarErrors([t('error.virheellinen-aikavali')]); - } else { - setCalendarErrors([]); - setHakuPaattyen(value?.toISOString() ?? null); - } -}; + useEffect(() => { + if ( + hakusanaChanged || + palveluChanged || + hakuAlkaenChanged || + hakuPaattyenChanged + ) { + setSeuraavatAlkaen(null); + } + }, [ + hakusanaChanged, + palveluChanged, + hakuAlkaenChanged, + hakuPaattyenChanged, + setSeuraavatAlkaen, + ]); + + const handleAlkuDateTimeChange = (value: dayjs.Dayjs | null) => { + setHakuAlkaen(value?.toISOString() ?? null); + }; + + const handleLoppuDateTimeChange = (value: dayjs.Dayjs | null) => { + if (hakuAlkaen && value && !dayjs(hakuAlkaen).isBefore(value)) { + setCalendarErrors([t('error.virheellinen-aikavali')]); + } else { + setCalendarErrors([]); + setHakuPaattyen(value?.toISOString() ?? null); + } + }; // päivitetään 3s viiveellä hakuparametrit const handleTypedSearch = useDebouncedCallback((term) => { @@ -151,21 +196,21 @@ const handleLoppuDateTimeChange = (value: dayjs.Dayjs | null) => { rightArrowIcon: { sx: { border: '1px solid', borderRadius: '50%' }, }, - } + }; return ( { value={dayjs(hakuAlkaen)} onChange={(newValue) => handleAlkuDateTimeChange(newValue)} aria-labelledby={labelId} - timeSteps = {{ minutes: 1}} + timeSteps={{ minutes: 1 }} slots={{ actionBar: CustomActionBar, - }} + }} slotProps={calendarSlotProps} /> ); @@ -243,10 +288,10 @@ const handleLoppuDateTimeChange = (value: dayjs.Dayjs | null) => { value={dayjs(hakuPaattyen)} onChange={(newValue) => handleLoppuDateTimeChange(newValue)} aria-labelledby={labelId} - timeSteps = {{ minutes: 1}} + timeSteps={{ minutes: 1 }} slots={{ actionBar: CustomActionBar, - }} + }} slotProps={calendarSlotProps} /> ); diff --git a/viestinvalitys-raportointi/src/app/components/OrganisaatioFilter.tsx b/viestinvalitys-raportointi/src/app/components/OrganisaatioFilter.tsx index 9216afdf..3bb8bd26 100644 --- a/viestinvalitys-raportointi/src/app/components/OrganisaatioFilter.tsx +++ b/viestinvalitys-raportointi/src/app/components/OrganisaatioFilter.tsx @@ -14,7 +14,7 @@ import { useTranslations } from 'next-intl'; export default function OrganisaatioFilter() { const [organisaatioHaku, setOrganisaatioHaku] = useQueryState( - 'orgSearchStr', + 'organisaatioHaku', NUQS_DEFAULT_OPTIONS, ); diff --git a/viestinvalitys-raportointi/src/app/components/OrganisaatioHierarkia.tsx b/viestinvalitys-raportointi/src/app/components/OrganisaatioHierarkia.tsx index d176b6bc..3cbd62e9 100644 --- a/viestinvalitys-raportointi/src/app/components/OrganisaatioHierarkia.tsx +++ b/viestinvalitys-raportointi/src/app/components/OrganisaatioHierarkia.tsx @@ -1,83 +1,42 @@ 'use client'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import { TreeItem, TreeView } from '@mui/x-tree-view'; +import { TreeItem2, TreeItem2Props, TreeItem2SlotProps } from '@mui/x-tree-view'; import { LanguageCode, Organisaatio } from '../lib/types'; -import { FormControl, FormControlLabel, Radio } from '@mui/material'; -import { ChangeEvent, SyntheticEvent } from 'react'; import { translateOrgName } from '../lib/util'; -import { useLocale, useTranslations } from 'next-intl'; +import React from 'react'; +import { RadioButtonChecked, RadioButtonUnchecked } from '@mui/icons-material'; -type Props = { - organisaatiot: Organisaatio[]; - selectedOid: string | undefined; - expandedOids: string[]; - handleSelect: (event: SyntheticEvent, nodeId: string) => void; - handleChange: (event: ChangeEvent) => void; - handleToggle: (event: SyntheticEvent, nodeIds: string[]) => void; -} - -const OrganisaatioHierarkia = ({ - organisaatiot, - selectedOid, - expandedOids, - handleSelect, - handleChange, - handleToggle, -}: Props) => { - const lng = useLocale() as LanguageCode; - const t = useTranslations(); - const renderTree = (org: Organisaatio) => { - if (!org) { - return null; - } - return ( - - { - handleChange(e); - }} - /> - } - /> - - } - > - {Array.isArray(org.children) - ? org.children.map((node) => renderTree(node)) - : null} - - ); - }; +const CustomTreeItem = React.forwardRef(function CustomTreeItem( + props: TreeItem2Props, + ref: React.Ref, +) { + return ( + , + checkedIcon: , + }, + } as TreeItem2SlotProps + } + /> + ); +}); +export const OrganisaatioTree = (org: Organisaatio, lng: LanguageCode) => { + if (!org) { + return null; + } return ( - <> - } - defaultExpandIcon={} - onNodeSelect={handleSelect} - onNodeToggle={handleToggle} - selected={selectedOid} - expanded={expandedOids} - > - {organisaatiot.map((org) => renderTree(org))} - - + + {Array.isArray(org.children) + ? org.children.map((node) => OrganisaatioTree(node, lng)) + : null} + ); }; - -export default OrganisaatioHierarkia; diff --git a/viestinvalitys-raportointi/src/app/components/OrganisaatioSelect.tsx b/viestinvalitys-raportointi/src/app/components/OrganisaatioSelect.tsx index 20126110..2efa0253 100644 --- a/viestinvalitys-raportointi/src/app/components/OrganisaatioSelect.tsx +++ b/viestinvalitys-raportointi/src/app/components/OrganisaatioSelect.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChangeEvent, SyntheticEvent, useState } from 'react'; -import { Drawer, IconButton, styled } from '@mui/material'; +import { useState } from 'react'; +import { Drawer, IconButton, Stack, styled } from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import { LanguageCode, Organisaatio } from '../lib/types'; import OrganisaatioFilter from './OrganisaatioFilter'; @@ -12,15 +12,16 @@ import { parseExpandedParents, translateOrgName, } from '../lib/util'; -import OrganisaatioHierarkia from './OrganisaatioHierarkia'; import { useQuery } from '@tanstack/react-query'; import { OphTypography } from '@opetushallitus/oph-design-system'; import { NUQS_DEFAULT_OPTIONS } from '../lib/constants'; import { ClientSpinner } from './ClientSpinner'; -import { getLocale } from 'next-intl/server'; import { useLocale, useTranslations } from 'next-intl'; +import { SimpleTreeView } from '@mui/x-tree-view'; +import React from 'react'; +import { OrganisaatioTree } from './OrganisaatioHierarkia'; -export const StyledDrawer = styled(Drawer)(({theme}) => ({ +export const StyledDrawer = styled(Drawer)(({ theme }) => ({ '& .MuiDrawer-paper': { padding: theme.spacing(2), }, @@ -34,10 +35,17 @@ const OrganisaatioSelect = () => { 'organisaatio', NUQS_DEFAULT_OPTIONS, ); - const [orgSearch, setOrgSearch] = useQueryState( - 'orgSearchStr', + const [organisaatioHaku, setOrganisaatioHaku] = useQueryState( + 'organisaatioHaku', NUQS_DEFAULT_OPTIONS, ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [seuraavatAlkaen, setSeuraavatAlkaen] = useQueryState( + 'seuraavatAlkaen', + NUQS_DEFAULT_OPTIONS, + ); + const t = useTranslations(); + const lng = useLocale() as LanguageCode; const toggleDrawer = (newOpen: boolean) => () => { setOpen(newOpen); @@ -45,26 +53,28 @@ const OrganisaatioSelect = () => { const searchOrgs = async (): Promise => { // kyselyä kutsutaan vain jos search-parametri on asetettu - const response = await searchOrganisaatio(orgSearch?.toString() ?? ''); + const response = await searchOrganisaatio(organisaatioHaku?.toString() ?? ''); if (response.organisaatiot?.length) { - await expandSearchMatches(response.organisaatiot); + await expandSearchMatches(response.organisaatiot, lng); } return response.organisaatiot ?? []; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { data, isLoading, error, refetch } = useQuery({ - queryKey: ['searchOrgs', orgSearch], + const { data, isLoading } = useQuery({ + queryKey: ['searchOrgs', organisaatioHaku], queryFn: () => searchOrgs(), - enabled: Boolean(orgSearch), + enabled: Boolean(organisaatioHaku), }); // matchataan käyttäjän asiointikielen nimellä - vain kälissä näkyvät osumat - const expandSearchMatches = async (foundOrgs: Organisaatio[]) => { - const locale = (await getLocale()) as LanguageCode; - if (orgSearch != null && foundOrgs?.length) { + const expandSearchMatches = async ( + foundOrgs: Organisaatio[], + locale: LanguageCode, + ) => { + if (organisaatioHaku != null && foundOrgs?.length) { const result: { oid: string; parentOidPath: string }[] = []; - collectOrgsWithMatchingName(foundOrgs, orgSearch ?? '', locale, result); + collectOrgsWithMatchingName(foundOrgs, organisaatioHaku ?? '', locale, result); const parentOids: Set = new Set(); for (const r of result) { const parents = parseExpandedParents(r.parentOidPath); @@ -75,47 +85,41 @@ const OrganisaatioSelect = () => { } }; - // TreeView-komponentin noden nagivointiklikkaus, ei varsinainen valinta - const handleSelect = ( - event: SyntheticEvent, - nodeId: string, + // nodet auki/kiinni, parametrit ja tyypitys Muin mukaan + const handleExpandedItemsChange = ( + event: React.SyntheticEvent, + itemIds: string[], ) => { - const index = expandedOids.indexOf(nodeId); - const copyExpanded = [...expandedOids]; - if (index === -1) { - copyExpanded.push(nodeId); - } else { - copyExpanded.splice(index, 1); - } - setExpandedOids(copyExpanded); + setExpandedOids(itemIds); }; - // nodet auki/kiinni - const handleToggle = ( - event: SyntheticEvent, - nodeIds: string[], + // käsitellään organisaation valinta radiobuttonilla + const handleSelectedChange = ( + event: React.SyntheticEvent, + itemId: string, + isSelected: boolean, ) => { - setExpandedOids(nodeIds); + if (isSelected) { + setSelectedOid(itemId); + setOpen(false); + const selectedOrgTemp = findOrganisaatioByOid( + data ?? [], + itemId, + ); + setSelectedOrg(selectedOrgTemp); + setOrganisaatioHaku(null); // nollataan hakusana + setSeuraavatAlkaen(null); // nollataan sivutus suodatuskriteerin muuttuessa + setExpandedOids(parseExpandedParents(selectedOrgTemp?.parentOidPath)); + } }; - // valittu organisaationode radiobuttonilla - const handleSelectedChange = (event: ChangeEvent) => { - setSelectedOid(event.target.value); - setOpen(false); - const selectedOrgTemp = findOrganisaatioByOid( - data ?? [], - event.target.value, - ); - setSelectedOrg(selectedOrgTemp); - setOrgSearch(null) - setExpandedOids(parseExpandedParents(selectedOrgTemp?.parentOidPath)); - }; - const t = useTranslations(); - const lng = useLocale() as LanguageCode; return ( <> - - {translateOrgName(selectedOrg, lng)} + + {translateOrgName(selectedOrg, lng)} @@ -126,22 +130,29 @@ const OrganisaatioSelect = () => { anchor="right" sx={{ padding: 2 }} > - - - {t('organisaatio.valittu', {organisaatioNimi: translateOrgName(selectedOrg, lng)})} - - {isLoading ? ( - - ) : ( - - )} + + + + {t('organisaatio.valittu', { + organisaatioNimi: translateOrgName(selectedOrg, lng), + })} + + {isLoading ? ( + + ) : ( + + {data?.map((org) => OrganisaatioTree(org, lng))} + + )} + ); diff --git a/viestinvalitys-raportointi/src/app/hooks/useHasChanged.ts b/viestinvalitys-raportointi/src/app/hooks/useHasChanged.ts new file mode 100644 index 00000000..3765ff5c --- /dev/null +++ b/viestinvalitys-raportointi/src/app/hooks/useHasChanged.ts @@ -0,0 +1,10 @@ +'use client'; +import { usePrevious } from '@/app/hooks/usePrevious'; + +export function useHasChanged( + value: T, + compare: (a?: T, b?: T) => boolean = (a, b) => a === b, +) { + const previousValue = usePrevious(value); + return !compare(value, previousValue); +} diff --git a/viestinvalitys-raportointi/src/app/hooks/usePrevious.ts b/viestinvalitys-raportointi/src/app/hooks/usePrevious.ts new file mode 100644 index 00000000..e8e04bcf --- /dev/null +++ b/viestinvalitys-raportointi/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; +} diff --git a/viestinvalitys-raportointi/src/app/lahetys/[tunniste]/VastaanottajaHaku.tsx b/viestinvalitys-raportointi/src/app/lahetys/[tunniste]/VastaanottajaHaku.tsx index 548322d5..2aa8225b 100644 --- a/viestinvalitys-raportointi/src/app/lahetys/[tunniste]/VastaanottajaHaku.tsx +++ b/viestinvalitys-raportointi/src/app/lahetys/[tunniste]/VastaanottajaHaku.tsx @@ -14,6 +14,8 @@ import { useDebouncedCallback } from 'use-debounce'; import { parseAsString, useQueryState, useQueryStates } from 'nuqs'; import { NUQS_DEFAULT_OPTIONS } from '@/app/lib/constants'; import { useTranslations } from 'next-intl'; +import { useHasChanged } from '@/app/hooks/useHasChanged'; +import { useEffect } from 'react'; const TilaSelect = ({ value: selectedTila, @@ -58,17 +60,37 @@ export default function VastaanottajaHaku() { const [vastaanottajaHaku, setVastaanottajahaku] = useQueryStates( { hakukentta: parseAsString.withDefault(''), - hakusana: parseAsString.withDefault('') - }, NUQS_DEFAULT_OPTIONS - ) + hakusana: parseAsString.withDefault(''), + }, + NUQS_DEFAULT_OPTIONS, + ); const [tila, setTila] = useQueryState('tila', NUQS_DEFAULT_OPTIONS); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [alkaen, setAlkaen] = useQueryState('alkaen', NUQS_DEFAULT_OPTIONS); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [hakusana, setHakusana] = useQueryState( + 'hakusana', + NUQS_DEFAULT_OPTIONS, + ); + const tilaChanged = useHasChanged(tila); + const hakusanaChanged = useHasChanged(hakusana); + + useEffect(() => { + if (hakusanaChanged || tilaChanged) { + setAlkaen(null); + } + }, [ + hakusanaChanged, + tilaChanged, + setAlkaen, + ]); // päivitetään 3s viiveellä hakuparametrit const handleTypedSearch = useDebouncedCallback((term) => { setVastaanottajahaku({ hakusana: term, - hakukentta: 'vastaanottaja' - }) + hakukentta: 'vastaanottaja', + }); }, 3000); return ( diff --git a/viestinvalitys-raportointi/src/app/lib/data.ts b/viestinvalitys-raportointi/src/app/lib/data.ts index 820116ff..90397699 100644 --- a/viestinvalitys-raportointi/src/app/lib/data.ts +++ b/viestinvalitys-raportointi/src/app/lib/data.ts @@ -1,9 +1,5 @@ 'use server'; // täytyy olla eksplisiittisesti koska käytetään client-komponentista react-querylla -import { - LahetysHakuParams, - OrganisaatioSearchResult, - VastaanottajatHakuParams, -} from './types'; +import { LahetysHakuParams, OrganisaatioSearchResult, VastaanottajatHakuParams } from './types'; import { apiUrl, virkailijaUrl } from './configurations'; import { makeRequest } from './http-client'; diff --git a/viestinvalitys-raportointi/src/app/lib/searchParams.ts b/viestinvalitys-raportointi/src/app/lib/searchParams.ts index cc65abbe..29ee2729 100644 --- a/viestinvalitys-raportointi/src/app/lib/searchParams.ts +++ b/viestinvalitys-raportointi/src/app/lib/searchParams.ts @@ -1,4 +1,4 @@ -import { createSearchParamsCache,parseAsString } from "nuqs/server"; +import { createSearchParamsCache, parseAsString } from "nuqs/server"; export const searchParamsCache = createSearchParamsCache({ seuraavatAlkaen: parseAsString,