diff --git a/package-lock.json b/package-lock.json index 386357f8..d59342ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,9 @@ "@emotion/cache": "^11.14.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", - "@mui/icons-material": "^6.2.1", - "@mui/material": "^6.2.1", - "@mui/material-nextjs": "^6.2.1", + "@mui/icons-material": "^6.4.0", + "@mui/material": "^6.4.0", + "@mui/material-nextjs": "^6.3.1", "@opetushallitus/oph-design-system": "github:opetushallitus/oph-design-system#v0.1.8", "@tanstack/react-query": "^5.62.8", "@tanstack/react-query-devtools": "^5.62.8", @@ -1319,9 +1319,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.2.1.tgz", - "integrity": "sha512-U/8vS1+1XiHBnnRRESSG1gvr6JDHdPjrpnW6KEebkAQWBn6wrpbSF/XSZ8/vJIRXH5NyDmMHi4Ro5Q70//JKhA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.0.tgz", + "integrity": "sha512-6u74wi+9zeNlukrCtYYET8Ed/n9AS27DiaXCZKAD3TRGFaqiyYSsQgN2disW83pI/cM1Q2lJY1JX4YfwvNtlNw==", "license": "MIT", "funding": { "type": "opencollective", @@ -1329,9 +1329,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.2.1.tgz", - "integrity": "sha512-bP0XtW+t5KFL+wjfQp2UctN/8CuWqF1qaxbYuCAsJhL+AzproM8gGOh2n8sNBcrjbVckzDNqaXqxdpn+OmoWug==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.0.tgz", + "integrity": "sha512-zF0Vqt8a+Zp2Oz8P+WvJflba6lLe3PhxIz1NNqn+n4A+wKLPbkeqY8ShmKjPyiCTg0RMbPrp993oUDl9xGsDlQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0" @@ -1344,7 +1344,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.2.1", + "@mui/material": "^6.4.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1355,16 +1355,16 @@ } }, "node_modules/@mui/material": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.2.1.tgz", - "integrity": "sha512-7VlKGsRKsy1bOSOPaSNgpkzaL+0C7iWAVKd2KYyAvhR9fTLJtiAMpq+KuzgEh1so2mtvQERN0tZVIceWMiIesw==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.0.tgz", + "integrity": "sha512-hNIgwdM9U3DNmowZ8mU59oFmWoDKjc92FqQnQva3Pxh6xRKWtD2Ej7POUHMX8Dwr1OpcSUlT2+tEMeLb7WYsIg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.2.1", - "@mui/system": "^6.2.1", - "@mui/types": "^7.2.20", - "@mui/utils": "^6.2.1", + "@mui/core-downloads-tracker": "^6.4.0", + "@mui/system": "^6.4.0", + "@mui/types": "^7.2.21", + "@mui/utils": "^6.4.0", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", @@ -1383,7 +1383,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.2.1", + "@mui/material-pigment-css": "^6.4.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1404,9 +1404,9 @@ } }, "node_modules/@mui/material-nextjs": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/material-nextjs/-/material-nextjs-6.2.1.tgz", - "integrity": "sha512-PiCsm5YVbWi+SgIAXvJidfX0m++Sri0aJiLe8cJZKnYoBCl7MT2mW/f73KmrNaFy1TmeXPKTg7EKu9f48o0eFg==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@mui/material-nextjs/-/material-nextjs-6.3.1.tgz", + "integrity": "sha512-14Y9wHdGsxI7u9XiMlpK5L6+MTsGo3Pod0EqwEde3jMx6dv63uqnMokhC1mzIJ3PjWtG8FwJkDsl57O9H6d+gQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0" @@ -1445,13 +1445,13 @@ "license": "MIT" }, "node_modules/@mui/private-theming": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.2.1.tgz", - "integrity": "sha512-u1y0gpcfrRRxCcIdVeU5eIvkinA82Q8ft178WUNYuoFQrsOrXdlBdZlRVi+eYuUFp1iXI55Cud7sMZZtETix5Q==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.0.tgz", + "integrity": "sha512-rNHci8MP6NOdEWAfZ/RBMO5Rhtp1T6fUDMSmingg9F1T6wiUeodIQ+NuTHh2/pMoUSeP9GdHdgMhMmfsXxOMuw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.2.1", + "@mui/utils": "^6.4.0", "prop-types": "^15.8.1" }, "engines": { @@ -1472,9 +1472,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.2.1.tgz", - "integrity": "sha512-6R3OgYw6zgCZWFYYMfxDqpGfJA78mUTOIlUDmmJlr60ogVNCrM87X0pqx5TbZ2OwUyxlJxN9qFgRr+J9H6cOBg==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.0.tgz", + "integrity": "sha512-ek/ZrDujrger12P6o4luQIfRd2IziH7jQod2WMbLqGE03Iy0zUwYmckRTVhRQTLPNccpD8KXGcALJF+uaUQlbg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -1506,16 +1506,16 @@ } }, "node_modules/@mui/system": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.2.1.tgz", - "integrity": "sha512-0lc8CbBP4WAAF+SmGMFJI9bpIyQvW3zvwIDzLsb26FIB/4Z0pO7qGe8mkAl0RM63Vb37899qxnThhHKgAAdy6w==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.0.tgz", + "integrity": "sha512-wTDyfRlaZCo2sW2IuOsrjeE5dl0Usrs6J7DxE3GwNCVFqS5wMplM2YeNiV3DO7s53RfCqbho+gJY6xaB9KThUA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.2.1", - "@mui/styled-engine": "^6.2.1", - "@mui/types": "^7.2.20", - "@mui/utils": "^6.2.1", + "@mui/private-theming": "^6.4.0", + "@mui/styled-engine": "^6.4.0", + "@mui/types": "^7.2.21", + "@mui/utils": "^6.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1546,9 +1546,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.20", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.20.tgz", - "integrity": "sha512-straFHD7L8v05l/N5vcWk+y7eL9JF0C2mtph/y4BPm3gn2Eh61dDwDB65pa8DLss3WJfDXYC7Kx5yjP0EmXpgw==", + "version": "7.2.21", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz", + "integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==", "license": "MIT", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1560,13 +1560,13 @@ } }, "node_modules/@mui/utils": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.2.1.tgz", - "integrity": "sha512-ubLqGIMhKUH2TF/Um+wRzYXgAooQw35th+DPemGrTpgrZHpOgcnUDIDbwsk1e8iQiuJ3mV/ErTtcQrecmlj5cg==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.0.tgz", + "integrity": "sha512-woOTATWNsTNR3YBh2Ixkj3l5RaxSiGoC9G8gOpYoFw1mZM77LWJeuMHFax7iIW4ahK0Cr35TF9DKtrafJmOmNQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/types": "^7.2.20", + "@mui/types": "^7.2.21", "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", diff --git a/package.json b/package.json index 764cc958..f19d8b23 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,9 @@ "@emotion/cache": "^11.14.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", - "@mui/icons-material": "^6.2.1", - "@mui/material": "^6.2.1", - "@mui/material-nextjs": "^6.2.1", + "@mui/icons-material": "^6.4.0", + "@mui/material": "^6.4.0", + "@mui/material-nextjs": "^6.3.1", "@opetushallitus/oph-design-system": "github:opetushallitus/oph-design-system#v0.1.8", "@tanstack/react-query": "^5.62.8", "@tanstack/react-query-devtools": "^5.62.8", diff --git a/src/app/components/action-bar.tsx b/src/app/components/action-bar.tsx index 0ec2f7c7..2a93f6bf 100644 --- a/src/app/components/action-bar.tsx +++ b/src/app/components/action-bar.tsx @@ -18,7 +18,7 @@ export const Button = withDefaultProps( }, })), { variant: 'text' }, -); +) as typeof OphButton; export const Container = styled(Box)(({ theme }) => ({ display: 'flex', diff --git a/src/app/components/download-button.tsx b/src/app/components/download-button.tsx deleted file mode 100644 index 0a401020..00000000 --- a/src/app/components/download-button.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { ButtonProps } from '@mui/material'; -import { UseMutationResult } from '@tanstack/react-query'; -import { SpinnerIcon } from './spinner-icon'; -import { OphButton } from '@opetushallitus/oph-design-system'; -import { FileDownloadOutlined } from '@mui/icons-material'; - -export const DownloadButton = ({ - mutation, - startIcon = , - disabled, - children, - spinner = , - Component = OphButton, -}: Pick & { - mutation: UseMutationResult; - spinner?: React.ReactNode; - Component?: React.ComponentType< - Pick - >; -}) => { - const { isPending, mutate } = mutation; - return ( - mutate()} - > - {children} - - ); -}; diff --git a/src/app/components/file-download-button.tsx b/src/app/components/file-download-button.tsx new file mode 100644 index 00000000..cf7cea00 --- /dev/null +++ b/src/app/components/file-download-button.tsx @@ -0,0 +1,50 @@ +import { FileResult } from '@/app/lib/http-client'; +import useToaster from '@/app/hooks/useToaster'; +import { useFileDownloadMutation } from '../hooks/useFileDownloadMutation'; +import { FileDownloadOutlined } from '@mui/icons-material'; +import { OphButton } from '@opetushallitus/oph-design-system'; +import { ButtonProps } from '@mui/material'; + +type FileDownloadProps = { + component?: typeof OphButton; + icon?: React.ReactNode; + getFile: () => Promise; + children: React.ReactNode; + defaultFileName: string; + errorKey: string; + errorMessage: string; +} & Omit; + +export function FileDownloadButton({ + defaultFileName, + children, + getFile, + errorKey, + errorMessage, + startIcon, + ...props +}: FileDownloadProps) { + const { addToast } = useToaster(); + const mutation = useFileDownloadMutation({ + onError: () => { + addToast({ + key: errorKey, + message: errorMessage, + type: 'error', + }); + }, + getFile, + defaultFileName, + }); + + return ( + } + loading={mutation.isPending} + onClick={() => mutation.mutate()} + {...props} + > + {children} + + ); +} diff --git a/src/app/components/localized-select.tsx b/src/app/components/localized-select.tsx index 3194a6b4..428cefc6 100644 --- a/src/app/components/localized-select.tsx +++ b/src/app/components/localized-select.tsx @@ -9,6 +9,7 @@ export const LocalizedSelect = ( return ( diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/harkinnanvaraiset/components/harkinnanvaraiset-action-bar.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/harkinnanvaraiset/components/harkinnanvaraiset-action-bar.tsx index aa2ff4c4..a2f9e6a8 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/harkinnanvaraiset/components/harkinnanvaraiset-action-bar.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/harkinnanvaraiset/components/harkinnanvaraiset-action-bar.tsx @@ -7,34 +7,10 @@ import { NoteOutlined, } from '@mui/icons-material'; import { ActionBar } from '@/app/components/action-bar'; -import { useMutation } from '@tanstack/react-query'; -import useToaster from '@/app/hooks/useToaster'; -import { DownloadButton } from '@/app/components/download-button'; import { getOsoitetarratHakemuksille } from '@/app/lib/valintalaskentakoostepalvelu'; -import { downloadBlob } from '@/app/lib/common'; import { HarkinnanvaraisetTilatByHakemusOids } from '@/app/lib/types/harkinnanvaraiset-types'; - -const useOsoitetarratMutation = ({ selection }: { selection: Set }) => { - const { addToast } = useToaster(); - - return useMutation({ - mutationFn: async () => { - const { fileName, blob } = await getOsoitetarratHakemuksille({ - tag: 'harkinnanvaraiset', - hakemusOids: Array.from(selection), - }); - downloadBlob(fileName ?? 'osoitetarrat.pdf', blob); - }, - onError: (e) => { - addToast({ - key: 'get-osoitetarrat', - message: 'harkinnanvaraiset.virhe-osoitetarrat', - type: 'error', - }); - console.error(e); - }, - }); -}; +import { FileDownloadButton } from '@/app/components/file-download-button'; +import { useCallback } from 'react'; const HyvaksyValitutButton = ({ selection, @@ -71,19 +47,27 @@ const OsoitetarratDownloadButton = ({ }) => { const { t } = useTranslation(); - const osoitetarratMutation = useOsoitetarratMutation({ - selection, - }); + const getFile = useCallback( + () => + getOsoitetarratHakemuksille({ + tag: 'harkinnanvaraiset', + hakemusOids: Array.from(selection), + }), + [selection], + ); return ( - } + defaultFileName="osoitetarrat.pdf" + getFile={getFile} + errorKey="get-osoitetarrat-error" + errorMessage="harkinnanvaraiset.virhe-osoitetarrat" > {t('harkinnanvaraiset.muodosta-osoitetarrat')} - + ); }; diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/pistesyotto/components/pistesyotto-actions.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/pistesyotto/components/pistesyotto-actions.tsx index 37868ad5..99cdd858 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/pistesyotto/components/pistesyotto-actions.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/pistesyotto/components/pistesyotto-actions.tsx @@ -1,8 +1,8 @@ import { useTranslations } from '@/app/hooks/useTranslations'; import { CircularProgress, Stack } from '@mui/material'; import { OphButton } from '@opetushallitus/oph-design-system'; -import { ExcelUploadButton } from './pistesyotto-excel-upload-button'; -import { ExcelDownloadButton } from './pistesyotto-excel-download-button'; +import { PistesyottoExcelUploadButton } from './pistesyotto-excel-upload-button'; +import { PistesyottoExcelDownloadButton } from './pistesyotto-excel-download-button'; export const PisteSyottoActions = ({ hakuOid, @@ -29,8 +29,8 @@ export const PisteSyottoActions = ({ {isUpdating && ( )} - - + + ); }; diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/pistesyotto/components/pistesyotto-excel-download-button.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/pistesyotto/components/pistesyotto-excel-download-button.tsx index fc77cfd2..83836465 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/pistesyotto/components/pistesyotto-excel-download-button.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/pistesyotto/components/pistesyotto-excel-download-button.tsx @@ -1,39 +1,8 @@ -import { DownloadButton } from '@/app/components/download-button'; -import useToaster from '@/app/hooks/useToaster'; +import { FileDownloadButton } from '@/app/components/file-download-button'; import { useTranslations } from '@/app/hooks/useTranslations'; -import { downloadBlob } from '@/app/lib/common'; import { getPistesyottoExcel } from '@/app/lib/valintalaskentakoostepalvelu'; -import { useMutation } from '@tanstack/react-query'; -const useExcelDownloadMutation = ({ - hakuOid, - hakukohdeOid, -}: { - hakuOid: string; - hakukohdeOid: string; -}) => { - const { addToast } = useToaster(); - - return useMutation({ - mutationFn: async () => { - const { fileName, blob } = await getPistesyottoExcel({ - hakuOid, - hakukohdeOid, - }); - downloadBlob(fileName ?? 'pistesyotto.xls', blob); - }, - onError: (e) => { - addToast({ - key: 'get-pistesyotto-excel', - message: 'pistesyotto.virhe-vie-taulukkolaskentaan', - type: 'error', - }); - console.error(e); - }, - }); -}; - -export const ExcelDownloadButton = ({ +export const PistesyottoExcelDownloadButton = ({ hakuOid, hakukohdeOid, }: { @@ -42,14 +11,14 @@ export const ExcelDownloadButton = ({ }) => { const { t } = useTranslations(); - const excelMutation = useExcelDownloadMutation({ - hakuOid, - hakukohdeOid, - }); - return ( - + getPistesyottoExcel({ hakuOid, hakukohdeOid })} + > {t('yleinen.vie-taulukkolaskentaan')} - + ); }; diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/pistesyotto/components/pistesyotto-excel-upload-button.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/pistesyotto/components/pistesyotto-excel-upload-button.tsx index 12d5e6eb..ca40cc00 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/pistesyotto/components/pistesyotto-excel-upload-button.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/pistesyotto/components/pistesyotto-excel-upload-button.tsx @@ -139,7 +139,7 @@ const FileSelector = forwardRef( }, ); -export const ExcelUploadButton = ({ +export const PistesyottoExcelUploadButton = ({ hakuOid, hakukohdeOid, }: { diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/valintakoekutsut/components/valintakoekutsut-action-bar.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/valintakoekutsut/components/valintakoekutsut-action-bar.tsx index 330c0d52..b05ab619 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/valintakoekutsut/components/valintakoekutsut-action-bar.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/valintakoekutsut/components/valintakoekutsut-action-bar.tsx @@ -7,43 +7,8 @@ import { GetValintakoeExcelParams, getValintakoeOsoitetarrat, } from '@/app/lib/valintalaskentakoostepalvelu'; -import { downloadBlob } from '@/app/lib/common'; -import { useMutation } from '@tanstack/react-query'; -import useToaster from '@/app/hooks/useToaster'; -import { DownloadButton } from '@/app/components/download-button'; -import { ValintakoekutsutDownloadProps } from '@/app/lib/types/valintakoekutsut-types'; import { ValintakoekutsutExcelDownloadButton } from './valintakoekutsut-excel-download-button'; - -const useOsoitetarratMutation = ({ - hakuOid, - hakukohdeOid, - valintakoeTunniste, - selection, -}: Omit & { - valintakoeTunniste: string; -}) => { - const { addToast } = useToaster(); - - return useMutation({ - mutationFn: async () => { - const { fileName, blob } = await getValintakoeOsoitetarrat({ - hakuOid, - hakukohdeOid, - valintakoeTunniste, - hakemusOids: selection && Array.from(selection), - }); - downloadBlob(fileName ?? 'osoitetarrat.pdf', blob); - }, - onError: (e) => { - addToast({ - key: 'get-osoitetarrat', - message: 'valintakoekutsut.virhe-osoitetarrat', - type: 'error', - }); - console.error(e); - }, - }); -}; +import { FileDownloadButton } from '@/app/components/file-download-button'; const OsoitetarratDownloadButton = ({ hakuOid, @@ -58,22 +23,25 @@ const OsoitetarratDownloadButton = ({ }) => { const { t } = useTranslation(); - const osoitetarratMutation = useOsoitetarratMutation({ - hakuOid, - hakukohdeOid, - valintakoeTunniste, - selection, - }); - return ( - } + getFile={() => + getValintakoeOsoitetarrat({ + hakuOid, + hakukohdeOid, + valintakoeTunniste, + hakemusOids: Array.from(selection), + }) + } + errorKey="get-osoitetarrat-error" + errorMessage="valintakoekutsut.virhe-osoitetarrat" + defaultFileName="osoitetarrat.pdf" > {t('valintakoekutsut.muodosta-osoitetarrat')} - + ); }; @@ -101,7 +69,7 @@ export const ValintakoekutsutActionBar = ({ { - const { addToast } = useToaster(); - - return useMutation({ - mutationFn: async () => { - const { fileName, blob } = await getValintakoeExcel({ - hakuOid, - hakukohdeOid, - valintakoeTunniste, - hakemusOids: selection && Array.from(selection), - }); - downloadBlob(fileName ?? 'valintakoekutsut.xls', blob); - }, - onError: (e) => { - addToast({ - key: 'get-valintakoe-excel', - message: 'valintakoekutsut.virhe-vie-taulukkolaskentaan', - type: 'error', - }); - console.error(e); - }, - }); -}; +import { OphButton } from '@opetushallitus/oph-design-system'; export const ValintakoekutsutExcelDownloadButton = ({ hakuOid, hakukohdeOid, valintakoeTunniste, selection, - Component, + component = OphButton, }: { hakuOid: string; hakukohdeOid: string; valintakoeTunniste: Array; selection?: Set; - Component?: React.ComponentType; + component?: typeof OphButton; }) => { const { t } = useTranslations(); - const excelMutation = useExcelDownloadMutation({ - hakuOid, - hakukohdeOid, - valintakoeTunniste, - selection, - }); - return ( - + getValintakoeExcel({ + hakuOid, + hakukohdeOid, + valintakoeTunniste, + hakemusOids: selection && Array.from(selection), + }) + } > {t('yleinen.vie-taulukkolaskentaan')} - + ); }; diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/valintalaskennan-tulokset/page.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/valintalaskennan-tulokset/page.tsx index 0c81edd4..84e967bf 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/valintalaskennan-tulokset/page.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/valintalaskennan-tulokset/page.tsx @@ -6,57 +6,37 @@ import { Box } from '@mui/material'; import { useTranslations } from '@/app/hooks/useTranslations'; import { useLasketutValinnanVaiheet } from '@/app/hooks/useLasketutValinnanVaiheet'; import { PageSizeSelector } from '@/app/components/table/page-size-selector'; -import React, { use } from 'react'; +import { use } from 'react'; import { ValintatapajonoContent } from './components/valintatapajono-content'; import { useJonosijatSearchParams } from '@/app/hooks/useJonosijatSearch'; import { FullClientSpinner } from '@/app/components/client-spinner'; -import { downloadBlob, isEmpty } from '@/app/lib/common'; -import { DownloadButton } from '@/app/components/download-button'; -import useToaster from '@/app/hooks/useToaster'; -import { useMutation } from '@tanstack/react-query'; +import { isEmpty } from '@/app/lib/common'; import { getValintalaskennanTulosExcel } from '@/app/lib/valintalaskentakoostepalvelu'; import { NoResults } from '@/app/components/no-results'; import { SearchInput } from '@/app/components/search-input'; +import { FileDownloadButton } from '@/app/components/file-download-button'; type LasketutValinnanvaiheetParams = { hakuOid: string; hakukohdeOid: string; }; -const useExcelDownloadMutation = ({ +const LaskennanTuloksetExcelDownloadButton = ({ hakukohdeOid, }: { hakukohdeOid: string; }) => { - const { addToast } = useToaster(); - - return useMutation({ - mutationFn: async () => { - const { fileName, blob } = await getValintalaskennanTulosExcel({ - hakukohdeOid, - }); - downloadBlob(fileName ?? 'valintalaskennan-tulokset.xls', blob); - }, - onError: (e) => { - addToast({ - key: 'get-valintakoe-excel', - message: - 'valintalaskennan-tulokset.virhe-vie-kaikki-taulukkolaskentaan', - type: 'error', - }); - console.error(e); - }, - }); -}; - -const ExcelDownloadButton = ({ hakukohdeOid }: { hakukohdeOid: string }) => { - const mutation = useExcelDownloadMutation({ hakukohdeOid }); const { t } = useTranslations(); return ( - + getValintalaskennanTulosExcel({ hakukohdeOid })} + > {t('valintalaskennan-tulokset.vie-kaikki-taulukkolaskentaan')} - + ); }; @@ -105,7 +85,7 @@ const LasketutValinnanVaiheetContent = ({ setSearchPhrase={setSearchPhrase} name="valintalaskennan-tulokset-search" /> - + diff --git a/src/app/haku/[oid]/henkilo/[hakemusOid]/components/valinnan-tilat-edit-modal.tsx b/src/app/haku/[oid]/henkilo/[hakemusOid]/components/valinnan-tilat-edit-modal.tsx index 4f815902..0e2a242d 100644 --- a/src/app/haku/[oid]/henkilo/[hakemusOid]/components/valinnan-tilat-edit-modal.tsx +++ b/src/app/haku/[oid]/henkilo/[hakemusOid]/components/valinnan-tilat-edit-modal.tsx @@ -5,7 +5,7 @@ import { } from '@/app/components/global-modal'; import { useTranslations } from '@/app/hooks/useTranslations'; import { Stack } from '@mui/material'; -import { OphButton, OphSelect } from '@opetushallitus/oph-design-system'; +import { OphButton } from '@opetushallitus/oph-design-system'; import { InlineFormControl, PaddedLabel } from './inline-form-control'; import useToaster from '@/app/hooks/useToaster'; import { @@ -29,6 +29,7 @@ import { ValinnanTulosUpdateErrorResult } from '@/app/lib/types/valinta-tulos-ty import { HttpClientResponse } from '@/app/lib/http-client'; import { EditModalDialog } from './edit-modal-dialog'; import { ValinnanTulosLisatiedoilla } from '../lib/henkilo-page-types'; +import { LocalizedSelect } from '@/app/components/localized-select'; const ModalActions = ({ onClose, @@ -204,7 +205,7 @@ export const ValinnanTilatEditModal = createModal<{ {t('henkilo.taulukko.vastaanoton-tila')} } renderInput={({ labelId }) => ( - } renderInput={({ labelId }) => ( - } renderInput={({ labelId }) => ( - {t('henkilo.taulukko.jarjestyskriteeri')} } renderInput={({ labelId }) => ( - void; + getFile: () => Promise; + defaultFileName: string; +}) { + return useMutation({ + mutationFn: async () => { + const { fileName, blob } = await getFile(); + downloadBlob(fileName ?? defaultFileName, blob); + }, + onError: (e) => { + onError(e); + console.error(e); + }, + }); +} diff --git a/src/app/lib/http-client.ts b/src/app/lib/http-client.ts index ae2e933e..3387e5e3 100644 --- a/src/app/lib/http-client.ts +++ b/src/app/lib/http-client.ts @@ -9,6 +9,23 @@ export type HttpClientResponse = { data: D; }; +const getContentFilename = (headers: Headers) => { + const contentDisposition = headers.get('content-disposition'); + return contentDisposition?.match(/ filename="(.*)"$/)?.[1]; +}; + +export type FileResult = { + fileName?: string; + blob: Blob; +}; + +export const createFileResult = async ( + response: HttpClientResponse, +): Promise => ({ + fileName: getContentFilename(response.headers), + blob: response.data, +}); + const doFetch = async (request: Request) => { try { const response = await fetch(request); diff --git a/src/app/lib/theme.tsx b/src/app/lib/theme.tsx index 26850b9f..dae43b3e 100644 --- a/src/app/lib/theme.tsx +++ b/src/app/lib/theme.tsx @@ -91,5 +91,10 @@ export const THEME_OVERRIDES: ThemeOptions = { }), }, }, + MuiButton: { + defaultProps: { + loadingPosition: 'start', + }, + }, }, }; diff --git a/src/app/lib/valintalaskentakoostepalvelu.ts b/src/app/lib/valintalaskentakoostepalvelu.ts index 6ca55946..1fc3632e 100644 --- a/src/app/lib/valintalaskentakoostepalvelu.ts +++ b/src/app/lib/valintalaskentakoostepalvelu.ts @@ -1,5 +1,5 @@ import { configuration } from './configuration'; -import { abortableClient, client, HttpClientResponse } from './http-client'; +import { abortableClient, client, createFileResult } from './http-client'; import { HenkilonValintaTulos } from './types/sijoittelu-types'; import { HakemuksenPistetiedot, @@ -30,6 +30,7 @@ import { HarkinnanvaraisuudenSyy } from './types/harkinnanvaraiset-types'; import { ValintakoeAvaimet } from './types/valintaperusteet-types'; import { Hakukohde } from './types/kouta-types'; import { getOpetuskieliCode } from './kouta'; +import { AssertionError } from 'assert'; export const getHakukohteenValintatuloksetIlmanHakijanTilaa = async ( hakuOid: string, @@ -259,30 +260,44 @@ export type GetValintakoeExcelParams = { valintakoeTunniste: Array; }; -const getContentFilename = (headers: Headers) => { - const contentDisposition = headers.get('content-disposition'); - return contentDisposition?.match(/ filename="(.*)"$/)?.[1]; -}; +const pollDocumentProcess = async (processId: string) => { + let pollTimes = 10; -const createFileResult = async (response: HttpClientResponse) => ({ - fileName: getContentFilename(response.headers), - blob: response.data, -}); + while (pollTimes) { + const processRes = await client.get<{ + dokumenttiId: string; + kasittelyssa: boolean; + keskeytetty: boolean; + kokonaistyo: { + valmis: boolean; + }; + poikkeukset: Array<{ + viesti: string; + }>; + }>(configuration.dokumenttiProsessiUrl({ id: processId })); + pollTimes -= 1; + + const { data } = processRes; + + if (data.kokonaistyo?.valmis || data.keskeytetty) { + return data; + } else if (pollTimes === 0) { + throw new OphApiError( + processRes, + 'Dokumentin prosessointi aikakatkaistiin', + ); + } + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + throw new AssertionError({ + message: 'Dokumentin prosessoinnin pollaus päättyi ilman tulosta!', + }); +}; const downloadProcessDocument = async (processId: string) => { - const processRes = await client.get<{ - dokumenttiId: string; - kasittelyssa: boolean; - keskeytetty: boolean; - kokonaistyo: { - valmis: boolean; - }; - poikkeukset: Array<{ - viesti: string; - }>; - }>(configuration.dokumenttiProsessiUrl({ id: processId })); + const data = await pollDocumentProcess(processId); - const { dokumenttiId, poikkeukset } = processRes.data; + const { dokumenttiId, poikkeukset } = data; if (!isEmpty(poikkeukset)) { const errorMessages = poikkeukset.map(prop('viesti')).join('\n'); @@ -292,6 +307,7 @@ const downloadProcessDocument = async (processId: string) => { const documentRes = await client.get( configuration.lataaDokumenttiUrl({ dokumenttiId }), ); + return createFileResult(documentRes); }; diff --git a/tests/e2e/pistesyotto.spec.ts b/tests/e2e/pistesyotto.spec.ts index 1a2a5029..3892a7d3 100644 --- a/tests/e2e/pistesyotto.spec.ts +++ b/tests/e2e/pistesyotto.spec.ts @@ -202,7 +202,7 @@ test.describe('Excel export', () => { ), async (route) => { await route.fulfill({ - json: { dokumenttiId: 'doc_id' }, + json: { dokumenttiId: 'doc_id', kokonaistyo: { valmis: true } }, }); }, ); diff --git a/tests/e2e/playwright-utils.ts b/tests/e2e/playwright-utils.ts index 657df78e..ea78aa91 100644 --- a/tests/e2e/playwright-utils.ts +++ b/tests/e2e/playwright-utils.ts @@ -1,7 +1,6 @@ import AxeBuilder from '@axe-core/playwright'; import { Locator, Page, Route, expect } from '@playwright/test'; import path from 'path'; -import cssEscape from 'css.escape'; export const expectPageAccessibilityOk = async (page: Page) => { const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); @@ -78,11 +77,11 @@ export async function selectOption( const combobox = (within ?? page).getByRole('combobox', { name: new RegExp(`^${name}`), }); - const contentId = await combobox.getAttribute('aria-controls'); + await combobox.click(); - const contentIdSelector = contentId ? `#${cssEscape(contentId)}` : ''; - // Selectin listbox rendataan portalilla juuritasolle - const listbox = page.locator(contentIdSelector); + + // Selectin listbox rendataan juuritasolle + const listbox = page.locator('#select-menu').getByRole('listbox'); await listbox .getByRole('option', { name: expectedOption, exact: true })