diff --git a/.env b/.env index 11d89ea6..78bc1be4 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ VIRKAILIJA_URL=https://virkailija.untuvaopintopolku.fi +NEXT_TELEMETRY_DISABLED=1 \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index bc7deb6f..ca9005b6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -6,6 +6,7 @@ import { FlatCompat } from '@eslint/eslintrc'; import ts from 'typescript-eslint'; import playwright from 'eslint-plugin-playwright'; import eslintConfigPrettier from 'eslint-config-prettier'; +import vitest from '@vitest/eslint-plugin'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -46,6 +47,16 @@ const config = ts.config( 'playwright/no-conditional-expect': 'off', }, }, + { + files: ['src/**/*.test.ts*'], // or any other pattern + plugins: { + vitest, + }, + rules: { + ...vitest.configs.recommended.rules, // you can also use vitest.configs.all.rules to enable all rules + 'vitest/max-nested-describe': ['error', { max: 2 }], // you can also modify rules' behavior using option like this + }, + }, ); export default config; diff --git a/next.config.mjs b/next.config.mjs index ffa63132..af2de5de 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -43,6 +43,7 @@ const nextConfig = { env: { VIRKAILIJA_URL: process.env.VIRKAILIJA_URL, APP_URL: process.env.APP_URL, + XSTATE_INSPECT: process.env.XSTATE_INSPECT, }, output: isStandalone ? 'standalone' : undefined, async redirects() { diff --git a/package-lock.json b/package-lock.json index c222bd66..386357f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,27 +10,27 @@ "dependencies": { "@date-fns/tz": "^1.2.0", "@ebay/nice-modal-react": "^1.2.13", - "@emotion/cache": "^11.13.5", - "@emotion/react": "^11.13.5", - "@emotion/styled": "^11.13.5", - "@mui/icons-material": "^6.1.10", - "@mui/material": "^6.1.10", - "@mui/material-nextjs": "^6.1.9", - "@opetushallitus/oph-design-system": "github:opetushallitus/oph-design-system#v0.1.7", - "@tanstack/react-query": "^5.62.3", - "@tanstack/react-query-devtools": "^5.62.3", + "@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", + "@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", "@xstate/react": "^5.0.0", "date-fns": "^4.1.0", - "i18next": "^24.0.5", + "i18next": "^24.2.0", "i18next-fetch-backend": "^6.0.0", - "next": "^15.0.4", + "next": "^15.1.2", "nextjs-toploader": "^3.7.15", "nuqs": "^2.2.3", "react": "^19.0.0", "react-dom": "^19.0.0", "react-error-boundary": "^4.1.2", - "react-i18next": "^15.1.3", - "remeda": "^2.17.4", + "react-i18next": "^15.2.0", + "remeda": "^2.18.0", "xstate": "^5.19.0" }, "devDependencies": { @@ -38,9 +38,11 @@ "@axe-core/react": "^4.10.1", "@eslint/compat": "^1.2.4", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.16.0", - "@next/eslint-plugin-next": "^15.0.4", - "@playwright/test": "^1.49.0", + "@eslint/js": "^9.17.0", + "@next/eslint-plugin-next": "^15.1.2", + "@opentelemetry/api": "^1.9.0", + "@playwright/test": "^1.49.1", + "@statelyai/inspect": "^0.4.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", @@ -48,30 +50,31 @@ "@types/css.escape": "^1.5.2", "@types/eslint__eslintrc": "^2.1.2", "@types/eslint-config-prettier": "^6.11.3", - "@types/node": "^22.10.1", + "@types/node": "^20.17.10", "@types/react": "^19", "@types/react-dom": "^19", - "@typescript-eslint/eslint-plugin": "^8.17.0", - "@typescript-eslint/parser": "^8.17.0", + "@typescript-eslint/eslint-plugin": "^8.18.1", + "@typescript-eslint/parser": "^8.18.1", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^2.1.8", + "@vitest/eslint-plugin": "^1.1.20", "autoprefixer": "^10.4.20", "babel-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124", "css.escape": "^1.5.1", "eslint": "^9", - "eslint-config-next": "^15.0.4", + "eslint-config-next": "^15.1.2", "eslint-config-prettier": "^9.1.0", "eslint-plugin-next": "^0.0.0", "eslint-plugin-playwright": "^2.1.0", "http-proxy-middleware": "^3.0.3", "husky": "^9.1.7", "jsdom": "^25.0.1", - "lint-staged": "^15.2.10", + "lint-staged": "^15.2.11", "postcss": "^8", "prettier": "^3.4.2", - "start-server-and-test": "^2.0.8", + "start-server-and-test": "^2.0.9", "typescript": "^5.7.2", - "typescript-eslint": "^8.17.0", + "typescript-eslint": "^8.18.1", "vitest": "^2.1.8" } }, @@ -508,9 +511,9 @@ } }, "node_modules/@emotion/cache": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.5.tgz", - "integrity": "sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0", @@ -540,16 +543,16 @@ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" }, "node_modules/@emotion/react": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.5.tgz", - "integrity": "sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.13.5", + "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" @@ -582,16 +585,16 @@ "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" }, "node_modules/@emotion/styled": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.5.tgz", - "integrity": "sha512-gnOQ+nGLPvDXgIx119JqGalys64lhMdnNQA9TMxhDA4K0Hq5+++OE20Zs5GxiCV9r814xQ2K5WmtofSpHVW6BQ==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", + "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/is-prop-valid": "^1.3.0", "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2" }, "peerDependencies": { @@ -611,9 +614,10 @@ "license": "MIT" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", - "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", "peerDependencies": { "react": ">=16.8.0" } @@ -752,9 +756,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", - "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", "dev": true, "license": "MIT", "engines": { @@ -1315,9 +1319,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.10.tgz", - "integrity": "sha512-LY5wdiLCBDY7u+Od8UmFINZFGN/5ZU90fhAslf/ZtfP+5RhuY45f679pqYIxe0y54l6Gkv9PFOc8Cs10LDTBYg==", + "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==", "license": "MIT", "funding": { "type": "opencollective", @@ -1325,9 +1329,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.10.tgz", - "integrity": "sha512-G6P1BCSt6EQDcKca47KwvKjlqgOXFbp2I3oWiOlFgKYTANBH89yk7ttMQ5ysqNxSYAB+4TdM37MlPYp4+FkVrQ==", + "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==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0" @@ -1340,7 +1344,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.1.10", + "@mui/material": "^6.2.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1351,22 +1355,22 @@ } }, "node_modules/@mui/material": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.10.tgz", - "integrity": "sha512-txnwYObY4N9ugv5T2n5h1KcbISegZ6l65w1/7tpSU5OB6MQCU94YkP8n/3slDw2KcEfRk4+4D8EUGfhSPMODEQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.2.1.tgz", + "integrity": "sha512-7VlKGsRKsy1bOSOPaSNgpkzaL+0C7iWAVKd2KYyAvhR9fTLJtiAMpq+KuzgEh1so2mtvQERN0tZVIceWMiIesw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.1.10", - "@mui/system": "^6.1.10", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.10", + "@mui/core-downloads-tracker": "^6.2.1", + "@mui/system": "^6.2.1", + "@mui/types": "^7.2.20", + "@mui/utils": "^6.2.1", "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.11", + "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^18.3.1", + "react-is": "^19.0.0", "react-transition-group": "^4.4.5" }, "engines": { @@ -1379,7 +1383,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.1.10", + "@mui/material-pigment-css": "^6.2.1", "@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" @@ -1400,9 +1404,9 @@ } }, "node_modules/@mui/material-nextjs": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@mui/material-nextjs/-/material-nextjs-6.1.9.tgz", - "integrity": "sha512-QIJANZt6tkRLoeRsIa0KoC4+MMywTIPQbthL2U2VXHLyrRan00+Yc2M+NFP85/EnPxNEUCRf19l4WKNaPtyetQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/material-nextjs/-/material-nextjs-6.2.1.tgz", + "integrity": "sha512-PiCsm5YVbWi+SgIAXvJidfX0m++Sri0aJiLe8cJZKnYoBCl7MT2mW/f73KmrNaFy1TmeXPKTg7EKu9f48o0eFg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0" @@ -1435,19 +1439,19 @@ } }, "node_modules/@mui/material/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", + "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==", "license": "MIT" }, "node_modules/@mui/private-theming": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.10.tgz", - "integrity": "sha512-DqgsH0XFEweeG3rQfVkqTkeXcj/E76PGYWag8flbPdV8IYdMo+DfVdFlZK8JEjsaIVD2Eu1kJg972XnH5pfnBQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.2.1.tgz", + "integrity": "sha512-u1y0gpcfrRRxCcIdVeU5eIvkinA82Q8ft178WUNYuoFQrsOrXdlBdZlRVi+eYuUFp1iXI55Cud7sMZZtETix5Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.1.10", + "@mui/utils": "^6.2.1", "prop-types": "^15.8.1" }, "engines": { @@ -1468,9 +1472,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.10.tgz", - "integrity": "sha512-+NV9adKZYhslJ270iPjf2yzdVJwav7CIaXcMlPSi1Xy1S/zRe5xFgZ6BEoMdmGRpr34lIahE8H1acXP2myrvRw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.2.1.tgz", + "integrity": "sha512-6R3OgYw6zgCZWFYYMfxDqpGfJA78mUTOIlUDmmJlr60ogVNCrM87X0pqx5TbZ2OwUyxlJxN9qFgRr+J9H6cOBg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -1502,16 +1506,16 @@ } }, "node_modules/@mui/system": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.10.tgz", - "integrity": "sha512-5YNIqxETR23SIkyP7MY2fFnXmplX/M4wNi2R+10AVRd3Ub+NLctWY/Vs5vq1oAMF0eSDLhRTGUjaUe+IGSfWqg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.2.1.tgz", + "integrity": "sha512-0lc8CbBP4WAAF+SmGMFJI9bpIyQvW3zvwIDzLsb26FIB/4Z0pO7qGe8mkAl0RM63Vb37899qxnThhHKgAAdy6w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.1.10", - "@mui/styled-engine": "^6.1.10", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.10", + "@mui/private-theming": "^6.2.1", + "@mui/styled-engine": "^6.2.1", + "@mui/types": "^7.2.20", + "@mui/utils": "^6.2.1", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1542,9 +1546,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.19", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", - "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", + "version": "7.2.20", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.20.tgz", + "integrity": "sha512-straFHD7L8v05l/N5vcWk+y7eL9JF0C2mtph/y4BPm3gn2Eh61dDwDB65pa8DLss3WJfDXYC7Kx5yjP0EmXpgw==", "license": "MIT", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1556,17 +1560,17 @@ } }, "node_modules/@mui/utils": { - "version": "6.1.10", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.10.tgz", - "integrity": "sha512-1ETuwswGjUiAf2dP9TkBy8p49qrw2wXa+RuAjNTRE5+91vtXJ1HKrs7H9s8CZd1zDlQVzUcUAPm9lpQwF5ogTw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.2.1.tgz", + "integrity": "sha512-ubLqGIMhKUH2TF/Um+wRzYXgAooQw35th+DPemGrTpgrZHpOgcnUDIDbwsk1e8iQiuJ3mV/ErTtcQrecmlj5cg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/types": "^7.2.19", - "@types/prop-types": "^15.7.13", + "@mui/types": "^7.2.20", + "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.3.1" + "react-is": "^19.0.0" }, "engines": { "node": ">=14.0.0" @@ -1586,21 +1590,21 @@ } }, "node_modules/@mui/utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", + "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==", "license": "MIT" }, "node_modules/@next/env": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.4.tgz", - "integrity": "sha512-WNRvtgnRVDD4oM8gbUcRc27IAhaL4eXQ/2ovGbgLnPGUvdyDr8UdXP4Q/IBDdAdojnD2eScryIDirv0YUCjUVw==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.2.tgz", + "integrity": "sha512-Hm3jIGsoUl6RLB1vzY+dZeqb+/kWPZ+h34yiWxW0dV87l8Im/eMOwpOA+a0L78U0HM04syEjXuRlCozqpwuojQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.0.4.tgz", - "integrity": "sha512-rbsF17XGzHtR7SDWzWpavSfum3/UdnF8bAaisnKwP//si3KWPTedVUsflAdjyK1zW3rweBjbALfKcavFneLGvg==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.2.tgz", + "integrity": "sha512-sgfw3+WdaYOGPKCvM1L+UucBmRfh8V2Ygefp7ELON0+0vY7uohQwXXnVWg3rY7mXDKharQR3o7uedpfvnU2hlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1638,9 +1642,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.4.tgz", - "integrity": "sha512-QecQXPD0yRHxSXWL5Ff80nD+A56sUXZG9koUsjWJwA2Z0ZgVQfuy7gd0/otjxoOovPVHR2eVEvPMHbtZP+pf9w==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.2.tgz", + "integrity": "sha512-b9TN7q+j5/7+rGLhFAVZiKJGIASuo8tWvInGfAd8wsULjB1uNGRCj1z1WZwwPWzVQbIKWFYqc+9L7W09qwt52w==", "cpu": [ "arm64" ], @@ -1654,9 +1658,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.4.tgz", - "integrity": "sha512-pb7Bye3y1Og3PlCtnz2oO4z+/b3pH2/HSYkLbL0hbVuTGil7fPen8/3pyyLjdiTLcFJ+ymeU3bck5hd4IPFFCA==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.2.tgz", + "integrity": "sha512-caR62jNDUCU+qobStO6YJ05p9E+LR0EoXh1EEmyU69cYydsAy7drMcOlUlRtQihM6K6QfvNwJuLhsHcCzNpqtA==", "cpu": [ "x64" ], @@ -1670,9 +1674,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.4.tgz", - "integrity": "sha512-12oSaBFjGpB227VHzoXF3gJoK2SlVGmFJMaBJSu5rbpaoT5OjP5OuCLuR9/jnyBF1BAWMs/boa6mLMoJPRriMA==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.2.tgz", + "integrity": "sha512-fHHXBusURjBmN6VBUtu6/5s7cCeEkuGAb/ZZiGHBLVBXMBy4D5QpM8P33Or8JD1nlOjm/ZT9sEE5HouQ0F+hUA==", "cpu": [ "arm64" ], @@ -1686,9 +1690,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.4.tgz", - "integrity": "sha512-QARO88fR/a+wg+OFC3dGytJVVviiYFEyjc/Zzkjn/HevUuJ7qGUUAUYy5PGVWY1YgTzeRYz78akQrVQ8r+sMjw==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.2.tgz", + "integrity": "sha512-9CF1Pnivij7+M3G74lxr+e9h6o2YNIe7QtExWq1KUK4hsOLTBv6FJikEwCaC3NeYTflzrm69E5UfwEAbV2U9/g==", "cpu": [ "arm64" ], @@ -1702,9 +1706,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.4.tgz", - "integrity": "sha512-Z50b0gvYiUU1vLzfAMiChV8Y+6u/T2mdfpXPHraqpypP7yIT2UV9YBBhcwYkxujmCvGEcRTVWOj3EP7XW/wUnw==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.2.tgz", + "integrity": "sha512-tINV7WmcTUf4oM/eN3Yuu/f8jQ5C6AkueZPKeALs/qfdfX57eNv4Ij7rt0SA6iZ8+fMobVfcFVv664Op0caCCg==", "cpu": [ "x64" ], @@ -1718,9 +1722,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.4.tgz", - "integrity": "sha512-7H9C4FAsrTAbA/ENzvFWsVytqRYhaJYKa2B3fyQcv96TkOGVMcvyS6s+sj4jZlacxxTcn7ygaMXUPkEk7b78zw==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.2.tgz", + "integrity": "sha512-jf2IseC4WRsGkzeUw/cK3wci9pxR53GlLAt30+y+B+2qAQxMw6WAC3QrANIKxkcoPU3JFh/10uFfmoMDF9JXKg==", "cpu": [ "x64" ], @@ -1734,9 +1738,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.4.tgz", - "integrity": "sha512-Z/v3WV5xRaeWlgJzN9r4PydWD8sXV35ywc28W63i37G2jnUgScA4OOgS8hQdiXLxE3gqfSuHTicUhr7931OXPQ==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.2.tgz", + "integrity": "sha512-wvg7MlfnaociP7k8lxLX4s2iBJm4BrNiNFhVUY+Yur5yhAJHfkS8qPPeDEUH8rQiY0PX3u/P7Q/wcg6Mv6GSAA==", "cpu": [ "arm64" ], @@ -1750,9 +1754,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.4.tgz", - "integrity": "sha512-NGLchGruagh8lQpDr98bHLyWJXOBSmkEAfK980OiNBa7vNm6PsNoPvzTfstT78WyOeMRQphEQ455rggd7Eo+Dw==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.2.tgz", + "integrity": "sha512-D3cNA8NoT3aWISWmo7HF5Eyko/0OdOO+VagkoJuiTk7pyX3P/b+n8XA/MYvyR+xSVcbKn68B1rY9fgqjNISqzQ==", "cpu": [ "x64" ], @@ -1807,9 +1811,19 @@ "node": ">=12.4.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opetushallitus/oph-design-system": { - "version": "0.1.7", - "resolved": "git+ssh://git@github.com/opetushallitus/oph-design-system.git#1dfeded92cf116a4a6e95fd6482db2f6359d19e0", + "version": "0.1.8", + "resolved": "git+ssh://git@github.com/opetushallitus/oph-design-system.git#4e56ec4f5ee689e1ae2b83c83aa7e7584d045892", "license": "EUPL-1.2", "peerDependencies": { "@mui/material": "^6", @@ -1832,13 +1846,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", - "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.49.0" + "playwright": "1.49.1" }, "bin": { "playwright": "cli.js" @@ -1919,6 +1933,24 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@statelyai/inspect": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@statelyai/inspect/-/inspect-0.4.0.tgz", + "integrity": "sha512-VxQldRlKYcu6rzLY83RSXVwMYexkH6hNx85B89YWYyXYWtNGaWHFCwV7a/Kz8FFPeUz8EKVAnyMOg2kNpn07wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-safe-stringify": "^2.1.1", + "isomorphic-ws": "^5.0.0", + "partysocket": "^0.0.25", + "safe-stable-stringify": "^2.4.3", + "superjson": "^1.13.3", + "uuid": "^9.0.1" + }, + "peerDependencies": { + "xstate": "^5.5.1" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -1926,18 +1958,18 @@ "license": "Apache-2.0" }, "node_modules/@swc/helpers": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", - "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "node_modules/@tanstack/query-core": { - "version": "5.62.3", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.3.tgz", - "integrity": "sha512-Jp/nYoz8cnO7kqhOlSv8ke/0MJRJVGuZ0P/JO9KQ+f45mpN90hrerzavyTKeSoT/pOzeoOUkv1Xd0wPsxAWXfg==", + "version": "5.62.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.8.tgz", + "integrity": "sha512-4fV31vDsUyvNGrKIOUNPrZztoyL187bThnoQOvAXEVlZbSiuPONpfx53634MKKdvsDir5NyOGm80ShFaoHS/mw==", "license": "MIT", "funding": { "type": "github", @@ -1955,12 +1987,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.62.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.3.tgz", - "integrity": "sha512-y2zDNKuhgiuMgsKkqd4AcsLIBiCfEO8U11AdrtAUihmLbRNztPrlcZqx2lH1GacZsx+y1qRRbCcJLYTtF1vKsw==", + "version": "5.62.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.8.tgz", + "integrity": "sha512-8TUstKxF/fysHonZsWg/hnlDVgasTdHx6Q+f1/s/oPKJBJbKUWPZEHwLTMOZgrZuroLMiqYKJ9w69Abm8mWP0Q==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.62.3" + "@tanstack/query-core": "5.62.8" }, "funding": { "type": "github", @@ -1971,9 +2003,9 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.62.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.62.3.tgz", - "integrity": "sha512-4iaQap/iP5ErS094u1WehFntHtjRo6g5HJMvyHovBVbsxnvgPc6AtKAw7qxPPoKy6Wj5Bew0045eYP5phiiBmw==", + "version": "5.62.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.62.8.tgz", + "integrity": "sha512-SwjXjQTRONd9WPeKVQQ9framG7YNqPV8PS+EGNVNXAyz2XThulMRCvZnh2+3DggnjcYM7YcpnuoZ4RH7q13p0g==", "license": "MIT", "dependencies": { "@tanstack/query-devtools": "5.61.4" @@ -1983,7 +2015,7 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.62.3", + "@tanstack/react-query": "^5.62.8", "react": "^18 || ^19" } }, @@ -2220,13 +2252,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "version": "20.17.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz", + "integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.19.2" } }, "node_modules/@types/parse-json": { @@ -2245,6 +2277,7 @@ "version": "19.0.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.0.tgz", "integrity": "sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -2261,26 +2294,26 @@ } }, "node_modules/@types/react-transition-group": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", - "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", "license": "MIT", - "dependencies": { + "peerDependencies": { "@types/react": "*" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz", - "integrity": "sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz", + "integrity": "sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.17.0", - "@typescript-eslint/type-utils": "8.17.0", - "@typescript-eslint/utils": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0", + "@typescript-eslint/scope-manager": "8.18.1", + "@typescript-eslint/type-utils": "8.18.1", + "@typescript-eslint/utils": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2295,25 +2328,21 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.17.0.tgz", - "integrity": "sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.1.tgz", + "integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.17.0", - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/typescript-estree": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0", + "@typescript-eslint/scope-manager": "8.18.1", + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/typescript-estree": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1", "debug": "^4.3.4" }, "engines": { @@ -2324,23 +2353,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz", - "integrity": "sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz", + "integrity": "sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0" + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2351,14 +2376,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.17.0.tgz", - "integrity": "sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz", + "integrity": "sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.17.0", - "@typescript-eslint/utils": "8.17.0", + "@typescript-eslint/typescript-estree": "8.18.1", + "@typescript-eslint/utils": "8.18.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2370,18 +2395,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz", - "integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz", + "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==", "dev": true, "license": "MIT", "engines": { @@ -2393,14 +2414,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz", - "integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz", + "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0", + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2415,10 +2436,8 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -2448,16 +2467,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.17.0.tgz", - "integrity": "sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.1.tgz", + "integrity": "sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.17.0", - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/typescript-estree": "8.17.0" + "@typescript-eslint/scope-manager": "8.18.1", + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/typescript-estree": "8.18.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2467,22 +2486,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz", - "integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz", + "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/types": "8.18.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2559,6 +2574,27 @@ } } }, + "node_modules/@vitest/eslint-plugin": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.20.tgz", + "integrity": "sha512-2eLsgUm+GVOpDfNyH2do//MiNO/WZkXrPi+EjDmXEdUt6Jwnziq4H221L8vJE0aJys+l1FRfSkm4QbaIyDCfBg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/utils": ">= 8.0", + "eslint": ">= 8.57.0", + "typescript": ">= 5.0.0", + "vitest": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", @@ -2763,6 +2799,7 @@ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "dev": true, + "license": "MIT", "dependencies": { "environment": "^1.0.0" }, @@ -3312,6 +3349,7 @@ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, + "license": "MIT", "dependencies": { "restore-cursor": "^5.0.0" }, @@ -3327,6 +3365,7 @@ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "dev": true, + "license": "MIT", "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" @@ -3343,6 +3382,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3354,13 +3394,15 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cli-truncate/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -3378,6 +3420,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -3448,7 +3491,8 @@ "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -3482,6 +3526,22 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -3622,9 +3682,10 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -3822,6 +3883,7 @@ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -4126,13 +4188,13 @@ } }, "node_modules/eslint-config-next": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.0.4.tgz", - "integrity": "sha512-97mLaAhbJKVQYXUBBrenRtEUAA6bNDPxWfaFEd6mEhKfpajP4wJrW4l7BUlHuYWxR8oQa9W014qBJpumpJQwWA==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.1.2.tgz", + "integrity": "sha512-PrMm1/4zWSJ689wd/ypWIR5ZF1uvmp3EkgpgBV1Yu6PhEobBjXMGgT8bVNelwl17LXojO8D5ePFRiI4qXjsPRA==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.0.4", + "@next/eslint-plugin-next": "15.1.2", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -4140,7 +4202,7 @@ "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { @@ -4474,6 +4536,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/eslint/node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -4591,6 +4663,19 @@ "through": "~2.3.1" } }, + "node_modules/event-target-shim": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", + "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "dev": true, @@ -4720,6 +4805,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.17.1", "dev": true, @@ -4933,10 +5025,11 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", - "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -5282,9 +5375,9 @@ } }, "node_modules/i18next": { - "version": "24.0.5", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.0.5.tgz", - "integrity": "sha512-1jSdEzgFPGLZRsQwydoMFCBBaV+PmrVEO5WhANllZPX4y2JSGTxUjJ+xVklHIsiS95uR8gYc/y0hYZWevucNjg==", + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz", + "integrity": "sha512-ArJJTS1lV6lgKH7yEf4EpgNZ7+THl7bsGxxougPYiXRTJ/Fe1j08/TBpV9QsXCIYVfdE/HWG/xLezJ5DOlfBOA==", "funding": [ { "type": "individual", @@ -5810,6 +5903,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -5822,6 +5928,16 @@ "dev": true, "license": "ISC" }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "dev": true, @@ -6076,7 +6192,9 @@ } }, "node_modules/lilconfig": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true, "license": "MIT", "engines": { @@ -6093,21 +6211,22 @@ "license": "MIT" }, "node_modules/lint-staged": { - "version": "15.2.10", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", - "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.11.tgz", + "integrity": "sha512-Ev6ivCTYRTGs9ychvpVw35m/bcNDuBN+mnTeObCL5h+boS5WzBEC6LHI4I9F/++sZm1m+J2LEiy0gxL/R9TBqQ==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "~5.3.0", "commander": "~12.1.0", - "debug": "~4.3.6", + "debug": "~4.4.0", "execa": "~8.0.1", - "lilconfig": "~3.1.2", - "listr2": "~8.2.4", + "lilconfig": "~3.1.3", + "listr2": "~8.2.5", "micromatch": "~4.0.8", "pidtree": "~0.6.0", "string-argv": "~0.3.2", - "yaml": "~2.5.0" + "yaml": "~2.6.1" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -6131,10 +6250,11 @@ } }, "node_modules/lint-staged/node_modules/yaml": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", - "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, @@ -6143,10 +6263,11 @@ } }, "node_modules/listr2": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", - "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", "dev": true, + "license": "MIT", "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -6164,6 +6285,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6176,6 +6298,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6187,19 +6310,22 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/listr2/node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/listr2/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -6217,6 +6343,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -6232,6 +6359,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -6273,6 +6401,7 @@ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, + "license": "MIT", "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", @@ -6292,6 +6421,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6304,6 +6434,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6315,13 +6446,15 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/log-update/node_modules/is-fullwidth-code-point": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "dev": true, + "license": "MIT", "dependencies": { "get-east-asian-width": "^1.0.0" }, @@ -6337,6 +6470,7 @@ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" @@ -6353,6 +6487,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -6370,6 +6505,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -6385,6 +6521,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -6536,6 +6673,7 @@ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -6609,14 +6747,14 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/next/-/next-15.0.4.tgz", - "integrity": "sha512-nuy8FH6M1FG0lktGotamQDCXhh5hZ19Vo0ht1AOIQWrYJLP598TIUagKtvJrfJ5AGwB/WmDqkKaKhMpVifvGPA==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/next/-/next-15.1.2.tgz", + "integrity": "sha512-nLJDV7peNy+0oHlmY2JZjzMfJ8Aj0/dd3jCwSZS8ZiO5nkQfcZRqDrRN3U5rJtqVTQneIOGZzb6LCNrk7trMCQ==", "license": "MIT", "dependencies": { - "@next/env": "15.0.4", + "@next/env": "15.1.2", "@swc/counter": "0.1.3", - "@swc/helpers": "0.5.13", + "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -6629,22 +6767,22 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.0.4", - "@next/swc-darwin-x64": "15.0.4", - "@next/swc-linux-arm64-gnu": "15.0.4", - "@next/swc-linux-arm64-musl": "15.0.4", - "@next/swc-linux-x64-gnu": "15.0.4", - "@next/swc-linux-x64-musl": "15.0.4", - "@next/swc-win32-arm64-msvc": "15.0.4", - "@next/swc-win32-x64-msvc": "15.0.4", + "@next/swc-darwin-arm64": "15.1.2", + "@next/swc-darwin-x64": "15.1.2", + "@next/swc-linux-arm64-gnu": "15.1.2", + "@next/swc-linux-arm64-musl": "15.1.2", + "@next/swc-linux-x64-gnu": "15.1.2", + "@next/swc-linux-x64-musl": "15.1.2", + "@next/swc-win32-arm64-msvc": "15.1.2", + "@next/swc-win32-x64-msvc": "15.1.2", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-66855b96-20241106 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-66855b96-20241106 || ^19.0.0", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "peerDependenciesMeta": { @@ -6905,6 +7043,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { "mimic-function": "^5.0.0" }, @@ -7007,6 +7146,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/partysocket": { + "version": "0.0.25", + "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-0.0.25.tgz", + "integrity": "sha512-1oCGA65fydX/FgdnsiBh68buOvfxuteoZVSb3Paci2kRp/7lhF0HyA8EDb5X/O6FxId1e+usPTQNRuzFEvkJbQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "event-target-shim": "^6.0.2" + } + }, "node_modules/path-exists": { "version": "4.0.0", "dev": true, @@ -7108,13 +7257,13 @@ } }, "node_modules/playwright": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", - "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.49.0" + "playwright-core": "1.49.1" }, "bin": { "playwright": "cli.js" @@ -7127,9 +7276,9 @@ } }, "node_modules/playwright-core": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", - "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -7298,9 +7447,9 @@ } }, "node_modules/react-i18next": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.3.tgz", - "integrity": "sha512-J11oA30FbM3NZegUZjn8ySK903z6PLBz/ZuBYyT1JMR0QPrW6PFXvl1WoUhortdGi9dM0m48/zJQlPskVZXgVw==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.2.0.tgz", + "integrity": "sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", @@ -7406,12 +7555,12 @@ } }, "node_modules/remeda": { - "version": "2.17.4", - "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.17.4.tgz", - "integrity": "sha512-pviU2Ag7Qx9mOCAKO4voxDx/scfLzdhp3v85qDO4xxntQsU76uE9sgrAAdK1ATn4zzaOJqCXYMMNRP+O9F4Wiw==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.18.0.tgz", + "integrity": "sha512-wvHvaApA7crz46HWhGTotPawkzd45w1iXzPK7r4ECQgbmcSqHLrVio2LOr7ZEp1pu5QDH1U391ZKu+qra7OoLw==", "license": "MIT", "dependencies": { - "type-fest": "^4.27.0" + "type-fest": "^4.30.0" } }, "node_modules/remeda/node_modules/type-fest": { @@ -7475,6 +7624,7 @@ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, + "license": "MIT", "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" @@ -7499,7 +7649,8 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/rollup": { "version": "4.14.0", @@ -7610,6 +7761,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -7801,6 +7962,7 @@ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" @@ -7817,6 +7979,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -7829,6 +7992,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -7880,16 +8044,16 @@ "license": "MIT" }, "node_modules/start-server-and-test": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.8.tgz", - "integrity": "sha512-v2fV6NV2F7tL1ocwfI4Wpait+IKjRbT5l3ZZ+ZikXdMLmxYsS8ynGAsCQAUVXkVyGyS+UibsRnvgHkMvJIvCsw==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.9.tgz", + "integrity": "sha512-DDceIvc4wdpr+z3Aqkot2QMho8TcUBh5qH0wEHDpEexBTzlheOcmh53d3dExABY4J5C7qS2UbSXqRWLtxpbWIQ==", "dev": true, "license": "MIT", "dependencies": { "arg": "^5.0.2", "bluebird": "3.7.2", "check-more-types": "2.24.0", - "debug": "4.3.7", + "debug": "4.4.0", "execa": "5.1.1", "lazy-ass": "1.6.0", "ps-tree": "1.2.0", @@ -8275,6 +8439,19 @@ "version": "4.2.0", "license": "MIT" }, + "node_modules/superjson": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz", + "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -8637,15 +8814,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.17.0.tgz", - "integrity": "sha512-409VXvFd/f1br1DCbuKNFqQpXICoTB+V51afcwG1pn1a3Cp92MqAUges3YjwEdQ0cMUoCIodjVDAYzyD8h3SYA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.1.tgz", + "integrity": "sha512-Mlaw6yxuaDEPQvb/2Qwu3/TfgeBHy9iTJ3mTwe7OvpPmF6KPQjVOfGyEJpPv6Ez2C34OODChhXrzYw/9phI0MQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.17.0", - "@typescript-eslint/parser": "8.17.0", - "@typescript-eslint/utils": "8.17.0" + "@typescript-eslint/eslint-plugin": "8.18.1", + "@typescript-eslint/parser": "8.18.1", + "@typescript-eslint/utils": "8.18.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8655,12 +8832,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/unbox-primitive": { @@ -8680,9 +8853,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, "license": "MIT" }, @@ -8735,6 +8908,20 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "5.2.7", "dev": true, diff --git a/package.json b/package.json index 7c66625a..764cc958 100644 --- a/package.json +++ b/package.json @@ -27,27 +27,27 @@ "dependencies": { "@date-fns/tz": "^1.2.0", "@ebay/nice-modal-react": "^1.2.13", - "@emotion/cache": "^11.13.5", - "@emotion/react": "^11.13.5", - "@emotion/styled": "^11.13.5", - "@mui/icons-material": "^6.1.10", - "@mui/material": "^6.1.10", - "@mui/material-nextjs": "^6.1.9", - "@opetushallitus/oph-design-system": "github:opetushallitus/oph-design-system#v0.1.7", - "@tanstack/react-query": "^5.62.3", - "@tanstack/react-query-devtools": "^5.62.3", + "@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", + "@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", "@xstate/react": "^5.0.0", "date-fns": "^4.1.0", - "i18next": "^24.0.5", + "i18next": "^24.2.0", "i18next-fetch-backend": "^6.0.0", - "next": "^15.0.4", + "next": "^15.1.2", "nextjs-toploader": "^3.7.15", "nuqs": "^2.2.3", "react": "^19.0.0", "react-dom": "^19.0.0", "react-error-boundary": "^4.1.2", - "react-i18next": "^15.1.3", - "remeda": "^2.17.4", + "react-i18next": "^15.2.0", + "remeda": "^2.18.0", "xstate": "^5.19.0" }, "devDependencies": { @@ -55,9 +55,11 @@ "@axe-core/react": "^4.10.1", "@eslint/compat": "^1.2.4", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.16.0", - "@next/eslint-plugin-next": "^15.0.4", - "@playwright/test": "^1.49.0", + "@eslint/js": "^9.17.0", + "@next/eslint-plugin-next": "^15.1.2", + "@opentelemetry/api": "^1.9.0", + "@playwright/test": "^1.49.1", + "@statelyai/inspect": "^0.4.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", @@ -65,30 +67,31 @@ "@types/css.escape": "^1.5.2", "@types/eslint__eslintrc": "^2.1.2", "@types/eslint-config-prettier": "^6.11.3", - "@types/node": "^22.10.1", + "@types/node": "^20.17.10", "@types/react": "^19", "@types/react-dom": "^19", - "@typescript-eslint/eslint-plugin": "^8.17.0", - "@typescript-eslint/parser": "^8.17.0", + "@typescript-eslint/eslint-plugin": "^8.18.1", + "@typescript-eslint/parser": "^8.18.1", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^2.1.8", + "@vitest/eslint-plugin": "^1.1.20", "autoprefixer": "^10.4.20", "babel-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124", "css.escape": "^1.5.1", "eslint": "^9", - "eslint-config-next": "^15.0.4", + "eslint-config-next": "^15.1.2", "eslint-config-prettier": "^9.1.0", "eslint-plugin-next": "^0.0.0", "eslint-plugin-playwright": "^2.1.0", "http-proxy-middleware": "^3.0.3", "husky": "^9.1.7", "jsdom": "^25.0.1", - "lint-staged": "^15.2.10", + "lint-staged": "^15.2.11", "postcss": "^8", "prettier": "^3.4.2", - "start-server-and-test": "^2.0.8", + "start-server-and-test": "^2.0.9", "typescript": "^5.7.2", - "typescript-eslint": "^8.17.0", + "typescript-eslint": "^8.18.1", "vitest": "^2.1.8" } } diff --git a/playwright.config.ts b/playwright.config.ts index e6b46b7f..854126ac 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, + forbidOnly: Boolean(process.env.CI), /* Retry on CI only */ retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 2 : undefined, diff --git a/src/app/(root)/components/haku-table.tsx b/src/app/(root)/components/haku-table.tsx index 871bb33a..09f8a2c3 100644 --- a/src/app/(root)/components/haku-table.tsx +++ b/src/app/(root)/components/haku-table.tsx @@ -6,6 +6,7 @@ import { ListTableColumn } from '@/app/components/table/table-types'; import { makeCountColumn } from '@/app/components/table/table-columns'; import { ListTable } from '@/app/components/table/list-table'; import { OphLink } from '@opetushallitus/oph-design-system'; +import { isTranslatedName } from '@/app/lib/localization/translation-utils'; export const HakuTable = ({ haut, @@ -25,7 +26,7 @@ export const HakuTable = ({ key: 'nimi', render: (haku) => ( - {typeof haku.nimi == 'object' + {isTranslatedName(haku.nimi) ? translateEntity(haku.nimi) : haku.nimi} diff --git a/src/app/components/error-alert.tsx b/src/app/components/error-alert.tsx new file mode 100644 index 00000000..df1e787a --- /dev/null +++ b/src/app/components/error-alert.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { + Accordion, + AccordionDetails, + AccordionSummary, + Alert, + Box, + Typography, +} from '@mui/material'; +import { useTranslations } from '@/app/hooks/useTranslations'; +import { useState } from 'react'; +import { styled } from '@/app/lib/theme'; +import { ArrowRight } from '@mui/icons-material'; + +const StyledAccordionSummary = styled(AccordionSummary)({ + flexDirection: 'row-reverse', + padding: 0, + '.Mui-expanded .custom-expand-icon': { + transform: 'rotate(-90deg)', + }, +}); + +const ErrorContent = ({ + message, +}: { + message: string | Array | undefined; +}) => { + return message + ? (Array.isArray(message) ? message : [message]).map((msg, index) => { + return ( + + {msg} + + ); + }) + : null; +}; + +export const ErrorAlert = ({ + title, + message, + hasAccordion = false, +}: { + title: string; + message: string | Array | undefined; + hasAccordion?: boolean; +}) => { + const [errorVisible, setErrorVisible] = useState(false); + const { t } = useTranslations(); + + return ( + + {title} + + {hasAccordion ? ( + + } + sx={{ flexDirection: 'row-reverse', padding: 0 }} + onClick={() => setErrorVisible(!errorVisible)} + > + + {errorVisible + ? t('valinnanhallinta.piilotavirhe') + : t('valinnanhallinta.naytavirhe')} + + + + + + + ) : ( + + )} + + + ); +}; diff --git a/src/app/components/error-with-icon.tsx b/src/app/components/error-with-icon.tsx new file mode 100644 index 00000000..9beeb5f2 --- /dev/null +++ b/src/app/components/error-with-icon.tsx @@ -0,0 +1,19 @@ +import { ErrorOutline } from '@mui/icons-material'; +import { Box, SvgIconProps } from '@mui/material'; + +export const ErrorWithIcon = ({ + children, + color, +}: { + children: React.ReactNode; + color?: SvgIconProps['color']; +}) => { + return ( + + + + {children} + + + ); +}; diff --git a/src/app/components/form/oph-input.tsx b/src/app/components/form/oph-input.tsx deleted file mode 100644 index 5458073a..00000000 --- a/src/app/components/form/oph-input.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client'; -import React from 'react'; -import { OutlinedInput, OutlinedInputProps } from '@mui/material'; - -export const OphInput = ({ - value, - onChange, - ...props -}: OutlinedInputProps & { - value: string; - onChange: (event: React.ChangeEvent) => void; - helperText?: string[]; -}) => { - return ( - - ); -}; diff --git a/src/app/components/oph-modal-dialog.tsx b/src/app/components/oph-modal-dialog.tsx index 578da239..437adefa 100644 --- a/src/app/components/oph-modal-dialog.tsx +++ b/src/app/components/oph-modal-dialog.tsx @@ -17,7 +17,7 @@ export type OphModalDialogProps = Pick< > & { titleAlign?: 'center' | 'left'; contentAlign?: 'center' | 'left'; - children: React.ReactNode; + children?: React.ReactNode; title: string; actions?: React.ReactNode; onClose?: ( diff --git a/src/app/components/pisteet-input.tsx b/src/app/components/pisteet-input.tsx index 54a0baa6..3fd8572c 100644 --- a/src/app/components/pisteet-input.tsx +++ b/src/app/components/pisteet-input.tsx @@ -5,10 +5,10 @@ import { numberValidator, } from '@/app/components/form/input-validators'; import { OphFormControl } from '@/app/components/form/oph-form-control'; -import { OphInput } from '@/app/components/form/oph-input'; import { useTranslations } from '@/app/hooks/useTranslations'; import { useState, ChangeEvent } from 'react'; import { type KoeInputsProps } from './koe-inputs'; +import { OphInput } from '@opetushallitus/oph-design-system'; export const PisteetInput = ({ koe, diff --git a/src/app/components/progress-bar.tsx b/src/app/components/progress-bar.tsx new file mode 100644 index 00000000..ec6c6786 --- /dev/null +++ b/src/app/components/progress-bar.tsx @@ -0,0 +1,47 @@ +import { Box } from '@mui/material'; +import { ophColors } from '@opetushallitus/oph-design-system'; +import { TRANSITION_DURATION } from '@/app/lib/constants'; + +const PROGRESSBAR_HEIGHT = '42px'; + +export const ProgressBar = ({ value }: { value: number }) => { + const valuePercent = `${value}%`; + return ( + theme.spacing(2), + userSelect: 'none', + }, + '&:before': { + backgroundColor: ophColors.white, + color: ophColors.grey900, + width: '100%', + }, + '&:after': { + backgroundColor: ophColors.cyan1, + color: ophColors.white, + width: valuePercent, + transition: `${TRANSITION_DURATION} width linear`, + }, + }} + /> + ); +}; diff --git a/src/app/components/react-query-client-provider.tsx b/src/app/components/react-query-client-provider.tsx index c9b56e80..4ba19f1e 100644 --- a/src/app/components/react-query-client-provider.tsx +++ b/src/app/components/react-query-client-provider.tsx @@ -17,6 +17,8 @@ export default function ReactQueryClientProvider({ retry: 1, throwOnError: true, staleTime: 10 * 1000, + refetchOnWindowFocus: false, + refetchOnReconnect: false, }, }, }), diff --git a/src/app/components/search-input.tsx b/src/app/components/search-input.tsx index f9f04d06..ea43f934 100644 --- a/src/app/components/search-input.tsx +++ b/src/app/components/search-input.tsx @@ -1,9 +1,10 @@ import { OphFormControl } from '@/app/components/form/oph-form-control'; import { useTranslations } from '@/app/hooks/useTranslations'; import { Search } from '@mui/icons-material'; -import { InputAdornment, OutlinedInput } from '@mui/material'; +import { InputAdornment } from '@mui/material'; import { ChangeEvent } from 'react'; import { styled } from '@/app/lib/theme'; +import { OphInput } from '@opetushallitus/oph-design-system'; const StyledContol = styled(OphFormControl)({ flexGrow: 0, @@ -39,7 +40,7 @@ export const SearchInput = ({ sx={sx ?? {}} label={t(label ?? 'hakeneet.hae')} renderInput={({ labelId }) => ( - { + const [isOpen, setIsOpen] = useState(false); + + const accordionId = useId(); + const contentId = `SimpleAccordionContent_${accordionId}`; + + return ( + + + } + onClick={() => setIsOpen((open) => !open)} + aria-controls={contentId} + aria-expanded={isOpen ? 'true' : 'false'} + > + {isOpen ? titleOpen : titleClosed} + + + {children} + + + ); +}; diff --git a/src/app/components/toaster.tsx b/src/app/components/toaster.tsx index 55bcf3ba..8b69395f 100644 --- a/src/app/components/toaster.tsx +++ b/src/app/components/toaster.tsx @@ -50,6 +50,7 @@ const InfoToast = ({ ); }; +// TODO: Korvaa ConfirmationModalDialog-komponentilla const ConfirmToast = ({ toast }: { toast: Toast }) => { const { t } = useTranslations(); const { removeToast } = useToaster(); diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index cca1d05f..5b6b5c9b 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -1,5 +1,6 @@ 'use client'; import { ErrorView } from './components/error-view'; +import { LocalizedThemeProvider } from './components/localized-theme-provider'; export default function GlobalError({ error, @@ -11,7 +12,9 @@ export default function GlobalError({ return ( - + + + ); diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/harkinnanvaraiset/components/harkinnanvaraiset-search-input.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/harkinnanvaraiset/components/harkinnanvaraiset-search-input.tsx index 553f30aa..d1d8b02f 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/harkinnanvaraiset/components/harkinnanvaraiset-search-input.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/harkinnanvaraiset/components/harkinnanvaraiset-search-input.tsx @@ -1,9 +1,10 @@ import { OphFormControl } from '@/app/components/form/oph-form-control'; import { Search } from '@mui/icons-material'; -import { InputAdornment, OutlinedInput } from '@mui/material'; +import { InputAdornment } from '@mui/material'; import { useHarkinnanvaraisetSearchParams } from '../hooks/useHarkinnanvaraisetSearchParams'; import { useTranslations } from '@/app/hooks/useTranslations'; import { ChangeEvent } from 'react'; +import { OphInput } from '@opetushallitus/oph-design-system'; export const HarkinnanvaraisetSearchInput = () => { const { searchPhrase, setSearchPhrase } = useHarkinnanvaraisetSearchParams(); @@ -20,7 +21,7 @@ export const HarkinnanvaraisetSearchInput = () => { textAlign: 'left', }} renderInput={({ labelId }) => ( - { - return ( - - - - {children} - - - ); -}; - export const PistesyottoTuontiError = ({ error }: { error: Error }) => { const { t } = useTranslations(); if (error instanceof OphApiError && Array.isArray(error.response.data)) { diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/sijoittelun-tulokset/components/sijoittelun-tila-cell.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/sijoittelun-tulokset/components/sijoittelun-tila-cell.tsx index 6f2f2bf0..56a75feb 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/sijoittelun-tulokset/components/sijoittelun-tila-cell.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/sijoittelun-tulokset/components/sijoittelun-tila-cell.tsx @@ -8,11 +8,14 @@ import { ChangeEvent, useState } from 'react'; import { SijoittelunTulosStyledCell } from './sijoittelun-tulos-styled-cell'; import { Box, InputAdornment, SelectChangeEvent, styled } from '@mui/material'; import { LocalizedSelect } from '@/app/components/localized-select'; -import { OphInput } from '@/app/components/form/oph-input'; import { isKorkeakouluHaku } from '@/app/lib/kouta'; import { Haku } from '@/app/lib/types/kouta-types'; import { SijoittelunTuloksetChangeEvent } from '../lib/sijoittelun-tulokset-state'; -import { ophColors, OphCheckbox } from '@opetushallitus/oph-design-system'; +import { + ophColors, + OphCheckbox, + OphInput, +} from '@opetushallitus/oph-design-system'; import { Language } from '@/app/lib/localization/localization-types'; import { getReadableHakemuksenTila } from '@/app/lib/sijoittelun-tulokset-utils'; diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/sijoittelun-tulokset/components/sijoittelun-tulos-error-modal.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/sijoittelun-tulokset/components/sijoittelun-tulos-error-modal.tsx index 4fe91744..ac4f22e2 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/sijoittelun-tulokset/components/sijoittelun-tulos-error-modal.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/sijoittelun-tulokset/components/sijoittelun-tulos-error-modal.tsx @@ -1,3 +1,4 @@ +import { ErrorWithIcon } from '@/app/components/error-with-icon'; import { ExternalLink } from '@/app/components/external-link'; import { createModal, useOphModalProps } from '@/app/components/global-modal'; import { OphModalDialog } from '@/app/components/oph-modal-dialog'; @@ -7,9 +8,7 @@ import { buildLinkToApplication } from '@/app/lib/ataru'; import { OphApiError } from '@/app/lib/common'; import { SijoittelunHakemusValintatiedoilla } from '@/app/lib/types/sijoittelu-types'; import { ValinnanTulosUpdateErrorResult } from '@/app/lib/types/valinta-tulos-types'; -import { Error } from '@mui/icons-material'; import { - Box, TableContainer, Table, TableHead, @@ -50,17 +49,6 @@ export const SijoittelunTulosErrorModalDialog = createModal( }, ); -const ErrorWithIcon = ({ children }: { children: string }) => { - return ( - - - - {children} - - - ); -}; - const SijoittelunTulosTallennusError = ({ error, hakemukset, diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/confirm.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/confirm.tsx index 2230ebcb..a2796ee5 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/confirm.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/confirm.tsx @@ -7,6 +7,7 @@ type ConfirmParams = { cancel: () => void; }; +// TODO: Korvaa ConfirmationModalDialog-komponentilla const Confirm = ({ confirm, cancel }: ConfirmParams) => { const { t } = useTranslations(); diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/error-row.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/error-row.tsx index fee678f8..b4ffc0fd 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/error-row.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/error-row.tsx @@ -1,99 +1,24 @@ 'use client'; -import { - Accordion, - AccordionDetails, - AccordionSummary, - Box, - TableCell, - TableRow, - Typography, -} from '@mui/material'; +import { TableCell, TableRow } from '@mui/material'; import { useTranslations } from '@/app/hooks/useTranslations'; -import { ophColors } from '@opetushallitus/oph-design-system'; -import { useState } from 'react'; -import { styled } from '@/app/lib/theme'; -import { ArrowRight, ErrorOutline } from '@mui/icons-material'; +import { ErrorAlert } from '@/app/components/error-alert'; type ErrorRowParams = { errorMessage: string | string[]; }; -const StyledAccordionSummary = styled(AccordionSummary)({ - flexDirection: 'row-reverse', - padding: 0, - '.Mui-expanded .custom-expand-icon': { - transform: 'rotate(-90deg)', - }, -}); - -const ErrorRow = ({ errorMessage }: ErrorRowParams) => { - const [showError, setShowError] = useState(false); - +export const ErrorRow = ({ errorMessage }: ErrorRowParams) => { const { t } = useTranslations(); - return ( - - - - - - - {t('valinnanhallinta.virhe')} - - - } - sx={{ flexDirection: 'row-reverse', padding: 0 }} - onClick={() => setShowError(!showError)} - > - - {showError - ? t('valinnanhallinta.piilotavirhe') - : t('valinnanhallinta.naytavirhe')} - - - - {Array.isArray(errorMessage) && ( - <> - {errorMessage.map((msg, index) => { - return ( - - {msg} - - ); - })} - - )} - {!Array.isArray(errorMessage) && ( - {errorMessage} - )} - - - - + + + ); }; - -export default ErrorRow; diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/hallinta-table-row.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/hallinta-table-row.tsx index b100da4f..e2323a02 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/hallinta-table-row.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/hallinta-table-row.tsx @@ -6,19 +6,16 @@ import { useTranslations } from '@/app/hooks/useTranslations'; import { OphButton } from '@opetushallitus/oph-design-system'; import Confirm from './confirm'; import { toFormattedDateTimeString } from '@/app/lib/localization/translation-utils'; -import ErrorRow from './error-row'; -import { useMachine } from '@xstate/react'; import { - LaskentaEvents, - LaskentaStates, - createLaskentaMachine, -} from '../lib/laskenta-state'; -import { useMemo } from 'react'; + LaskentaEventType, + LaskentaState, + useLaskentaState, +} from '@/app/lib/state/laskenta-state'; import { Haku, Hakukohde } from '@/app/lib/types/kouta-types'; -import { sijoitellaankoHaunHakukohteetLaskennanYhteydessa } from '@/app/lib/kouta'; import { useToaster } from '@/app/hooks/useToaster'; import { Valinnanvaihe } from '@/app/lib/types/valintaperusteet-types'; import { HaunAsetukset } from '@/app/lib/types/haun-asetukset'; +import { ErrorRow } from './error-row'; type HallintaTableRowParams = { haku: Haku; @@ -39,48 +36,28 @@ const HallintaTableRow = ({ areAllLaskentaRunning, lastCalculated, }: HallintaTableRowParams) => { - const { t, translateEntity } = useTranslations(); + const { t } = useTranslations(); const { addToast } = useToaster(); - const laskentaMachine = useMemo(() => { - return createLaskentaMachine( - { - haku, - hakukohde, - sijoitellaanko: sijoitellaankoHaunHakukohteetLaskennanYhteydessa( - haku, - haunAsetukset, - ), - valinnanvaiheTyyppi: vaihe.tyyppi, - valinnanvaiheNumber: index, - valinnanvaiheNimi: vaihe.nimi, - translateEntity, - }, - addToast, - ); - }, [ + const [state, send] = useLaskentaState({ haku, - hakukohde, haunAsetukset, - translateEntity, - index, - vaihe.tyyppi, + hakukohteet: hakukohde, + vaihe, addToast, - vaihe.nimi, - ]); - - const [state, send] = useMachine(laskentaMachine); + valinnanvaiheNumber: index, + }); const start = () => { - send({ type: LaskentaEvents.START }); + send({ type: LaskentaEventType.START }); }; const cancelConfirmation = () => { - send({ type: LaskentaEvents.CANCEL }); + send({ type: LaskentaEventType.CANCEL }); }; const confirm = async () => { - send({ type: LaskentaEvents.CONFIRM }); + send({ type: LaskentaEventType.CONFIRM }); }; return ( @@ -109,7 +86,7 @@ const HallintaTableRow = ({ {t(vaihe.tyyppi)} {isLaskentaUsedForValinnanvaihe(vaihe) && - !state.matches(LaskentaStates.WAITING_CONFIRMATION) && ( + !state.matches(LaskentaState.WAITING_CONFIRMATION) && ( start()} > {t('valinnanhallinta.kaynnista')} - {state.matches(LaskentaStates.PROCESSING) && ( + {state.matches(LaskentaState.PROCESSING) && ( @@ -134,7 +111,7 @@ const HallintaTableRow = ({ )} {isLaskentaUsedForValinnanvaihe(vaihe) && - state.matches(LaskentaStates.WAITING_CONFIRMATION) && ( + state.matches(LaskentaState.WAITING_CONFIRMATION) && ( )} {!isLaskentaUsedForValinnanvaihe(vaihe) && !vaihe.valisijoittelu && ( diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/hallinta-table.tsx b/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/hallinta-table.tsx index d0a505be..3ba053d8 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/hallinta-table.tsx +++ b/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/components/hallinta-table.tsx @@ -17,21 +17,18 @@ import { import { useTranslations } from '@/app/hooks/useTranslations'; import { Haku, Hakukohde } from '@/app/lib/types/kouta-types'; import HallintaTableRow from './hallinta-table-row'; -import { sijoitellaankoHaunHakukohteetLaskennanYhteydessa } from '@/app/lib/kouta'; import Confirm from './confirm'; import { getHakukohteenLasketutValinnanvaiheet } from '@/app/lib/valintalaskenta-service'; -import ErrorRow from './error-row'; import { toFormattedDateTimeString } from '@/app/lib/localization/translation-utils'; import { - LaskentaEvents, - LaskentaStates, - createLaskentaMachine, -} from '../lib/laskenta-state'; -import { useMachine } from '@xstate/react'; -import { useMemo } from 'react'; + LaskentaEventType, + LaskentaState, + useLaskentaState, +} from '@/app/lib/state/laskenta-state'; import { useToaster } from '@/app/hooks/useToaster'; import { OphButton, OphTypography } from '@opetushallitus/oph-design-system'; import { HaunAsetukset } from '@/app/lib/types/haun-asetukset'; +import { ErrorRow } from './error-row'; type HallintaTableParams = { haku: Haku; @@ -44,25 +41,15 @@ const HallintaTable = ({ haku, haunAsetukset, }: HallintaTableParams) => { - const { t, translateEntity } = useTranslations(); + const { t } = useTranslations(); const { addToast } = useToaster(); - const laskentaMachine = useMemo(() => { - return createLaskentaMachine( - { - haku, - hakukohde, - sijoitellaanko: sijoitellaankoHaunHakukohteetLaskennanYhteydessa( - haku, - haunAsetukset, - ), - translateEntity, - }, - addToast, - ); - }, [haku, hakukohde, haunAsetukset, translateEntity, addToast]); - - const [state, send] = useMachine(laskentaMachine); + const [state, send] = useLaskentaState({ + haku, + haunAsetukset, + hakukohteet: hakukohde, + addToast, + }); const [valinnanvaiheetQuery, lasketutValinnanvaiheetQuery] = useSuspenseQueries({ @@ -79,15 +66,15 @@ const HallintaTable = ({ }); const confirm = async () => { - send({ type: LaskentaEvents.CONFIRM }); + send({ type: LaskentaEventType.CONFIRM }); }; const start = () => { - send({ type: LaskentaEvents.START }); + send({ type: LaskentaEventType.START }); }; const cancel = () => { - send({ type: LaskentaEvents.CANCEL }); + send({ type: LaskentaEventType.CANCEL }); }; if (valinnanvaiheetQuery.data.length === 0) { @@ -128,7 +115,7 @@ const HallintaTable = ({ (a) => a.valinnanvaiheoid === vaihe.oid, )?.createdAt } - areAllLaskentaRunning={state.matches(LaskentaStates.PROCESSING)} + areAllLaskentaRunning={state.matches(LaskentaState.PROCESSING)} /> ))} @@ -144,12 +131,12 @@ const HallintaTable = ({ rowGap: 2, }} > - {!state.matches(LaskentaStates.WAITING_CONFIRMATION) && ( + {!state.matches(LaskentaState.WAITING_CONFIRMATION) && ( isLaskentaUsedForValinnanvaihe(vaihe), ) || @@ -159,10 +146,10 @@ const HallintaTable = ({ {t('valinnanhallinta.kaynnistakaikki')} )} - {state.matches(LaskentaStates.WAITING_CONFIRMATION) && ( + {state.matches(LaskentaState.WAITING_CONFIRMATION) && ( )} - {state.matches(LaskentaStates.PROCESSING) && ( + {state.matches(LaskentaState.PROCESSING) && ( )} {containsValisijoittelu && ( diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/lib/laskenta-state.ts b/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/lib/laskenta-state.ts deleted file mode 100644 index e133aa06..00000000 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/lib/laskenta-state.ts +++ /dev/null @@ -1,292 +0,0 @@ -'use client'; - -import { assign, fromPromise, setup } from 'xstate'; -import { Laskenta, laskentaReducer } from './valinnan-hallinta-types'; -import { ValinnanvaiheTyyppi } from '@/app/lib/types/valintaperusteet-types'; -import { Haku, Hakukohde } from '@/app/lib/types/kouta-types'; -import { TranslatedName } from '@/app/lib/localization/localization-types'; -import { - getLaskennanSeurantaTiedot, - getLaskennanTilaHakukohteelle, - kaynnistaLaskenta, - kaynnistaLaskentaHakukohteenValinnanvaiheille, -} from '@/app/lib/valintalaskenta-service'; -import { FetchError } from '@/app/lib/common'; -import { Toast } from '@/app/hooks/useToaster'; -import { - LaskentaErrorSummary, - LaskentaStart, - SeurantaTiedot, -} from '@/app/lib/types/laskenta-types'; - -const POLLING_INTERVAL = 5000; - -export type StartLaskentaParams = { - haku: Haku; - hakukohde: Hakukohde; - valinnanvaiheTyyppi?: ValinnanvaiheTyyppi; - sijoitellaanko: boolean; - valinnanvaiheNumber?: number; - valinnanvaiheNimi?: string; - translateEntity: (translateable: TranslatedName) => string; -}; - -export type LaskentaContext = { - laskenta: Laskenta; - startLaskentaParams: StartLaskentaParams; - seurantaTiedot: SeurantaTiedot | null; - errorSummary: LaskentaErrorSummary | null; - error?: Error; -}; - -export enum LaskentaStates { - IDLE = 'IDLE', - WAITING_CONFIRMATION = 'WAITING_CONFIRMATION', - STARTING = 'STARTING', - PROCESSING = 'PROCESSING', - PROCESSING_FETCHING = 'FETCHING', - PROCESSING_WAITING = 'WAITING', - PROCESSING_DETERMINE_POLL_COMPLETION = 'DETERMINE_POLL_COMPLETION', - FETCHING_SUMMARY = 'FETCHING_SUMMARY', - DETERMINE_SUMMARY = 'DETERMINE_SUMMARY', - ERROR_LASKENTA = 'ERROR_LASKENTA', - COMPLETED = 'COMPLETED', -} - -export enum LaskentaEvents { - START = 'START', - CONFIRM = 'CONFIRM', - CANCEL = 'CANCEL', -} - -const tryAndParseError = async (wrappedFn: () => Promise) => { - try { - return await wrappedFn(); - } catch (e) { - if (e instanceof FetchError) { - const message = e.message; - throw message; - } - throw e; - } -}; - -export const createLaskentaMachine = ( - params: StartLaskentaParams, - addToast: (toast: Toast) => void, -) => { - return setup({ - types: { - context: {} as LaskentaContext, - }, - actors: { - startLaskenta: fromPromise( - ({ input }: { input: StartLaskentaParams }) => { - return tryAndParseError(async () => { - if (input.valinnanvaiheTyyppi && input.valinnanvaiheNumber) { - return await kaynnistaLaskenta( - input.haku, - input.hakukohde, - input.valinnanvaiheTyyppi, - input.sijoitellaanko, - input.valinnanvaiheNumber, - input.translateEntity, - ); - } else { - return await kaynnistaLaskentaHakukohteenValinnanvaiheille( - input.haku, - input.hakukohde, - input.sijoitellaanko, - input.translateEntity, - ); - } - }); - }, - ), - pollLaskenta: fromPromise(({ input }: { input: Laskenta }) => { - return tryAndParseError(async () => { - if (input.runningLaskenta) { - return await getLaskennanSeurantaTiedot( - input.runningLaskenta.loadingUrl, - ); - } - throw 'Tried to fetch seurantatiedot without having access to running laskenta'; - }); - }), - fetchSummary: fromPromise(({ input }: { input: Laskenta }) => { - return tryAndParseError(async () => { - if (input.runningLaskenta) { - return getLaskennanTilaHakukohteelle( - input.runningLaskenta.loadingUrl, - ); - } - throw 'Tried to fetch summary without having access to laskenta'; - }); - }), - }, - }).createMachine({ - id: `LaskentaMachine-${params.hakukohde.oid}-${params.valinnanvaiheNumber ?? ''}`, - initial: LaskentaStates.IDLE, - context: { - laskenta: {}, - startLaskentaParams: params, - seurantaTiedot: null, - errorSummary: null, - }, - states: { - [LaskentaStates.IDLE]: { - on: { - [LaskentaEvents.START]: { - target: LaskentaStates.WAITING_CONFIRMATION, - }, - }, - }, - [LaskentaStates.WAITING_CONFIRMATION]: { - on: { - [LaskentaEvents.CONFIRM]: { - target: LaskentaStates.STARTING, - actions: assign({ - laskenta: {}, - errorSummary: null, - seurantaTiedot: null, - error: undefined, - }), - }, - [LaskentaEvents.CANCEL]: { - target: LaskentaStates.IDLE, - }, - }, - }, - [LaskentaStates.STARTING]: { - invoke: { - src: 'startLaskenta', - input: ({ context }) => context.startLaskentaParams, - onDone: { - target: 'PROCESSING.FETCHING', - actions: assign({ - laskenta: ({ event, context }) => - laskentaReducer(context.laskenta, { - runningLaskenta: event.output, - }), - }), - }, - onError: { - target: LaskentaStates.ERROR_LASKENTA, - actions: assign({ - error: ({ event }) => event.error as Error, - }), - }, - }, - }, - [LaskentaStates.PROCESSING]: { - initial: LaskentaStates.PROCESSING_FETCHING, - states: { - [LaskentaStates.PROCESSING_FETCHING]: { - invoke: { - src: 'pollLaskenta', - input: ({ context }) => context.laskenta, - onDone: { - target: LaskentaStates.PROCESSING_DETERMINE_POLL_COMPLETION, - actions: assign({ - seurantaTiedot: ({ event }) => event.output, - }), - }, - onError: { - target: '#ERROR_LASKENTA', - actions: assign({ - error: ({ event }) => event.error as Error, - }), - }, - }, - }, - [LaskentaStates.PROCESSING_WAITING]: { - after: { - [POLLING_INTERVAL]: LaskentaStates.PROCESSING_FETCHING, - }, - }, - [LaskentaStates.PROCESSING_DETERMINE_POLL_COMPLETION]: { - always: [ - { - guard: ({ context }) => - context.seurantaTiedot?.tila === 'VALMIS', - target: '#FETCHING_SUMMARY', - }, - { - target: LaskentaStates.PROCESSING_WAITING, - }, - ], - }, - }, - }, - [LaskentaStates.FETCHING_SUMMARY]: { - id: 'FETCHING_SUMMARY', - invoke: { - src: 'fetchSummary', - input: ({ context }) => context.laskenta, - onDone: { - target: LaskentaStates.DETERMINE_SUMMARY, - actions: assign({ - errorSummary: ({ event }) => event.output, - }), - }, - onError: { - target: LaskentaStates.ERROR_LASKENTA, - actions: assign({ - error: ({ event }) => event.error as Error, - }), - }, - }, - }, - [LaskentaStates.DETERMINE_SUMMARY]: { - always: [ - { - guard: ({ context }) => - (context.seurantaTiedot != null && - context.seurantaTiedot.hakukohteitaKeskeytetty > 0) || - (context.errorSummary?.notifications?.length ?? 0) > 0, - target: LaskentaStates.ERROR_LASKENTA, - actions: assign({ - laskenta: ({ context }) => - laskentaReducer(context.laskenta, { - errorMessage: context.errorSummary?.notifications, - }), - }), - }, - { - target: LaskentaStates.COMPLETED, - }, - ], - }, - [LaskentaStates.ERROR_LASKENTA]: { - id: 'ERROR_LASKENTA', - always: [{ target: LaskentaStates.IDLE }], - }, - [LaskentaStates.COMPLETED]: { - always: [{ target: LaskentaStates.IDLE }], - entry: [ - assign({ - laskenta: ({ context }) => - laskentaReducer(context.laskenta, { - calculatedTime: new Date(), - }), - }), - ({ context }) => { - const wholeHakukohde: boolean = - !context.startLaskentaParams.valinnanvaiheNimi; - const keyPartValinnanvaihe = wholeHakukohde - ? '' - : `-${context.startLaskentaParams.valinnanvaiheNumber ?? 0}`; - const key = `laskenta-completed-for-${context.startLaskentaParams.hakukohde.oid}${keyPartValinnanvaihe}`; - const message = wholeHakukohde - ? 'valinnanhallinta.valmis' - : 'valinnanhallinta.valmisvalinnanvaihe'; - const messageParams = wholeHakukohde - ? undefined - : { nimi: context.startLaskentaParams.valinnanvaiheNimi ?? '' }; - addToast({ key, message, type: 'success', messageParams }); - }, - ], - }, - }, - }); -}; diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/lib/valinnan-hallinta-types.ts b/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/lib/valinnan-hallinta-types.ts deleted file mode 100644 index 51b1adb2..00000000 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/lib/valinnan-hallinta-types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { LaskentaStart } from '@/app/lib/types/laskenta-types'; - -export type Laskenta = { - errorMessage?: string | string[] | null; - calculatedTime?: Date | number | null; - runningLaskenta?: LaskentaStart; -}; - -export const laskentaReducer = ( - state: Laskenta, - action: Laskenta, -): Laskenta => { - return Object.assign({}, state, action); -}; diff --git a/src/app/haku/[oid]/hakukohde/components/hakukohde-search.tsx b/src/app/haku/[oid]/hakukohde/components/hakukohde-search.tsx index 2a1ed0e1..cd1d1b2e 100644 --- a/src/app/haku/[oid]/hakukohde/components/hakukohde-search.tsx +++ b/src/app/haku/[oid]/hakukohde/components/hakukohde-search.tsx @@ -1,7 +1,8 @@ import { useHakukohdeSearchParams } from '@/app/hooks/useHakukohdeSearch'; import { useTranslations } from '@/app/hooks/useTranslations'; import { Search } from '@mui/icons-material'; -import { FormControl, InputAdornment, OutlinedInput } from '@mui/material'; +import { FormControl, InputAdornment } from '@mui/material'; +import { OphInput } from '@opetushallitus/oph-design-system'; import { ChangeEvent } from 'react'; export const HakukohdeSearch = () => { @@ -18,7 +19,7 @@ export const HakukohdeSearch = () => { paddingRight: 2, }} > - void; +}) => { + const { t } = useTranslations(); + return ( + + { + onAnswer(true); + }} + > + {t('yleinen.kylla')} + + { + onAnswer(false); + }} + > + {t('yleinen.ei')} + + + } + /> + ); +}; diff --git a/src/app/haku/[oid]/henkilo/[hakemusOid]/components/henkilon-pistesyotto.tsx b/src/app/haku/[oid]/henkilo/[hakemusOid]/components/henkilon-pistesyotto.tsx index 291e7c95..4827a433 100644 --- a/src/app/haku/[oid]/henkilo/[hakemusOid]/components/henkilon-pistesyotto.tsx +++ b/src/app/haku/[oid]/henkilo/[hakemusOid]/components/henkilon-pistesyotto.tsx @@ -85,7 +85,6 @@ const KokeenPistesyotto = ({ /> } disabled={isUpdating} onClick={() => { diff --git a/src/app/haku/[oid]/henkilo/[hakemusOid]/components/henkilon-valintalaskenta.tsx b/src/app/haku/[oid]/henkilo/[hakemusOid]/components/henkilon-valintalaskenta.tsx new file mode 100644 index 00000000..998f80c4 --- /dev/null +++ b/src/app/haku/[oid]/henkilo/[hakemusOid]/components/henkilon-valintalaskenta.tsx @@ -0,0 +1,200 @@ +'use client'; + +import { useTranslations } from '@/app/hooks/useTranslations'; +import { Divider, Stack, styled, Typography } from '@mui/material'; +import { OphButton, OphTypography } from '@opetushallitus/oph-design-system'; +import { withDefaultProps } from '@/app/lib/mui-utils'; +import { + LaskentaActorRef, + LaskentaEvent, + LaskentaEventType, + LaskentaMachineSnapshot, + LaskentaState, + useLaskentaError, + useLaskentaState, +} from '@/app/lib/state/laskenta-state'; +import { HenkilonHakukohdeTuloksilla } from '../lib/henkilo-page-types'; +import useToaster from '@/app/hooks/useToaster'; +import { HaunAsetukset } from '@/app/lib/types/haun-asetukset'; +import { Haku } from '@/app/lib/types/kouta-types'; +import { ErrorAlert } from '@/app/components/error-alert'; +import { useSelector } from '@xstate/react'; +import { SeurantaTiedot } from '@/app/lib/types/laskenta-types'; +import { TFunction } from 'i18next'; +import { ProgressBar } from '@/app/components/progress-bar'; +import { SuorittamattomatHakukohteet } from './suorittamattomat-hakukohteet'; +import { ConfirmationModalDialog } from './confirmation-modal-dialog'; + +const LaskentaButton = withDefaultProps( + styled(OphButton)({ + alignSelf: 'flex-start', + }), + { + variant: 'contained', + }, +); + +const LaskentaStateButton = ({ + state, + send, +}: { + state: LaskentaMachineSnapshot; + send: (event: LaskentaEvent) => void; +}) => { + const { t } = useTranslations(); + + switch (true) { + case state.hasTag('stopped') && !state.hasTag('completed'): + return ( + { + send({ type: LaskentaEventType.START }); + }} + > + {t('henkilo.suorita-valintalaskenta')} + + ); + case state.hasTag('started'): + return ( + { + send({ type: LaskentaEventType.CANCEL }); + }} + > + {t('henkilo.keskeyta-valintalaskenta')} + + ); + case state.hasTag('completed'): + return ( + { + send({ type: LaskentaEventType.RESET_RESULTS }); + }} + > + {t('henkilo.sulje-laskennan-tiedot')} + + ); + default: + return null; + } +}; + +const getLaskentaStatusText = ( + state: LaskentaMachineSnapshot, + seurantaTiedot: SeurantaTiedot | null, + t: TFunction, +) => { + switch (true) { + case state.hasTag('canceling') || + (state.matches(LaskentaState.FETCHING_SUMMARY) && + state.context.seurantaTiedot?.tila === 'PERUUTETTU'): + return `${t('henkilo.keskeytetaan-laskentaa')} `; + case state.matches(LaskentaState.STARTING) || + (state.hasTag('started') && seurantaTiedot == null): + return `${t('henkilo.kaynnistetaan-laskentaa')} `; + case state.hasTag('started'): + return seurantaTiedot?.jonosija + ? `${'henkilo.tehtava-on-laskennassa-jonosijalla'} ${seurantaTiedot?.jonosija}. ` + : `${t('henkilo.tehtava-on-laskennassa-parhaillaan')}. `; + case state.hasTag('completed'): + return `${t('henkilo.laskenta-on-paattynyt')}. `; + default: + return ''; + } +}; + +const LaskentaResult = ({ actorRef }: { actorRef: LaskentaActorRef }) => { + const { t } = useTranslations(); + + const laskentaError = useLaskentaError(actorRef); + + const state = useSelector(actorRef, (s) => s); + const seurantaTiedot = useSelector(actorRef, (s) => s.context.seurantaTiedot); + + const laskentaPercent = seurantaTiedot + ? Math.round( + (100 * + (seurantaTiedot?.hakukohteitaValmiina + + seurantaTiedot?.hakukohteitaKeskeytetty)) / + seurantaTiedot?.hakukohteitaYhteensa, + ) + : 0; + + switch (true) { + case state.matches({ [LaskentaState.IDLE]: LaskentaState.ERROR }): + return ( + + ); + case state.hasTag('started') || state.hasTag('completed'): + return ( + <> + + {t('henkilo.valintalaskenta')} + + + + {getLaskentaStatusText(state, seurantaTiedot, t)} + {seurantaTiedot && + `${t('henkilo.hakukohteita-valmiina')} ${seurantaTiedot.hakukohteitaValmiina}/${seurantaTiedot.hakukohteitaYhteensa}. ` + + `${t('henkilo.suorittamattomia-hakukohteita')} ${seurantaTiedot?.hakukohteitaKeskeytetty ?? 0}.`} + + + ); + default: + return null; + } +}; + +export const HenkilonValintalaskenta = ({ + haku, + haunAsetukset, + hakukohteet, +}: { + haku: Haku; + haunAsetukset: HaunAsetukset; + hakukohteet: Array; +}) => { + const { addToast } = useToaster(); + + const [state, send, actorRef] = useLaskentaState({ + haku, + haunAsetukset, + hakukohteet, + addToast, + }); + + return ( + + { + if (answer) { + send({ type: LaskentaEventType.CONFIRM }); + } else { + send({ type: LaskentaEventType.CANCEL }); + } + }} + /> + + + {state.hasTag('completed') && ( + + )} + {(state.hasTag('started') || state.hasTag('completed')) && ( + + )} + + ); +}; diff --git a/src/app/haku/[oid]/henkilo/[hakemusOid]/components/suorittamattomat-hakukohteet.tsx b/src/app/haku/[oid]/henkilo/[hakemusOid]/components/suorittamattomat-hakukohteet.tsx new file mode 100644 index 00000000..ba1062c3 --- /dev/null +++ b/src/app/haku/[oid]/henkilo/[hakemusOid]/components/suorittamattomat-hakukohteet.tsx @@ -0,0 +1,76 @@ +import { ErrorWithIcon } from '@/app/components/error-with-icon'; +import { SimpleAccordion } from '@/app/components/simple-accordion'; +import { useTranslations } from '@/app/hooks/useTranslations'; +import { NDASH } from '@/app/lib/constants'; +import { LaskentaActorRef } from '@/app/lib/state/laskenta-state'; +import { Hakukohde } from '@/app/lib/types/kouta-types'; +import { Box, Stack, Typography } from '@mui/material'; +import { useSelector } from '@xstate/react'; + +export const SuorittamattomatHakukohteet = ({ + actorRef, + hakukohteet, +}: { + actorRef: LaskentaActorRef; + hakukohteet: Array; +}) => { + const { t, translateEntity } = useTranslations(); + + const summaryIlmoitus = useSelector( + actorRef, + (s) => s.context.summary?.ilmoitus, + ); + + const summaryErrors = useSelector(actorRef, (s) => + s.context.summary?.hakukohteet.filter((hk) => hk?.tila !== 'VALMIS'), + ); + + return summaryErrors ? ( + + + {summaryErrors?.map((error) => { + const hakukohde = hakukohteet.find( + (hk) => hk.oid === error.hakukohdeOid, + ); + const ilmoitukset = error.ilmoitukset; + return ( + + <> + + {translateEntity(hakukohde?.jarjestyspaikkaHierarkiaNimi)} + {` ${NDASH} `} + {translateEntity(hakukohde?.nimi)} ({error.hakukohdeOid}) + + + + {t('henkilo.syy')}: + + + {(error.ilmoitukset?.length ?? 0) > 0 ? ( + ilmoitukset?.map((ilmoitus) => ( + + {ilmoitus?.otsikko} + + )) + ) : ( + + {error.tila === 'TEKEMATTA' && summaryIlmoitus + ? summaryIlmoitus?.otsikko + : error.tila} + + )} + + + + + ); + })} + + + ) : null; +}; diff --git a/src/app/haku/[oid]/henkilo/[hakemusOid]/components/valintalaskenta-edit-modal.tsx b/src/app/haku/[oid]/henkilo/[hakemusOid]/components/valintalaskenta-edit-modal.tsx index c69bac68..c0ab3fe4 100644 --- a/src/app/haku/[oid]/henkilo/[hakemusOid]/components/valintalaskenta-edit-modal.tsx +++ b/src/app/haku/[oid]/henkilo/[hakemusOid]/components/valintalaskenta-edit-modal.tsx @@ -4,9 +4,12 @@ import { useOphModalProps, } from '@/app/components/global-modal'; import { useTranslations } from '@/app/hooks/useTranslations'; -import { OphButton, OphSelect } from '@opetushallitus/oph-design-system'; +import { + OphButton, + OphInput, + OphSelect, +} from '@opetushallitus/oph-design-system'; import { Stack } from '@mui/material'; -import { OphInput } from '@/app/components/form/oph-input'; import { useEffect, useState } from 'react'; import { LaskettuJono } from '@/app/hooks/useLasketutValinnanVaiheet'; import { diff --git a/src/app/haku/[oid]/henkilo/[hakemusOid]/page.tsx b/src/app/haku/[oid]/henkilo/[hakemusOid]/page.tsx index 18e16e67..1e9a5731 100644 --- a/src/app/haku/[oid]/henkilo/[hakemusOid]/page.tsx +++ b/src/app/haku/[oid]/henkilo/[hakemusOid]/page.tsx @@ -12,6 +12,9 @@ import { HakutoiveetTable } from './components/hakutoiveet-table'; import { useHenkiloPageData } from './hooks/useHenkiloPageData'; import { use } from 'react'; import { HenkilonPistesyotto } from './components/henkilon-pistesyotto'; +import { useHaunAsetukset } from '@/app/hooks/useHaunAsetukset'; +import { useHaku } from '@/app/hooks/useHaku'; +import { HenkilonValintalaskenta } from './components/henkilon-valintalaskenta'; const HenkiloContent = ({ hakuOid, @@ -22,6 +25,9 @@ const HenkiloContent = ({ }) => { const { t, translateEntity } = useTranslations(); + const { data: haku } = useHaku({ hakuOid }); + const { data: haunAsetukset } = useHaunAsetukset({ hakuOid }); + const { hakukohteet, hakija, postitoimipaikka } = useHenkiloPageData({ hakuOid, hakemusOid, @@ -30,7 +36,12 @@ const HenkiloContent = ({ return ( <> {getHenkiloTitle(hakija)} - + + { const { searchPhrase, setSearchPhrase } = useHenkiloSearchParams(); @@ -22,7 +23,7 @@ export const HenkiloSearch = () => { helperText={t('henkilo.hae-helpertext')} renderInput={() => { return ( - { const [searchPhrase, setSearchPhrase] = useQueryState( @@ -37,20 +39,57 @@ export const useHakukohdeSearchResults = (hakuOid: string) => { getHakukohteetQueryOptions(hakuOid, userPermissions), ); + const sortedHakukohteet = useMemo(() => { + return sortBy(hakukohteet, (hakukohde: Hakukohde) => + translateEntity(hakukohde.nimi), + ); + }, [hakukohteet, translateEntity]); + + const hakukohdeMatchTargets = useMemo( + () => + sortedHakukohteet.map((hakukohde) => + toLowerCase( + translateEntity(hakukohde.nimi) + + '#' + + translateEntity(hakukohde.jarjestyspaikkaHierarkiaNimi), + ), + ), + [sortedHakukohteet, translateEntity], + ); + const { searchPhrase } = useHakukohdeSearchParams(); + const searchPhraseWords = useMemo( + () => + searchPhrase + .toLowerCase() + .split(/\s+/) + .filter((s) => !isEmpty(s)), + [searchPhrase], + ); + const results = useMemo(() => { - return hakukohteet.filter( - (hakukohde: Hakukohde) => - translateEntity(hakukohde.nimi) - .toLowerCase() - .includes(searchPhrase?.toLowerCase() ?? '') || - translateEntity(hakukohde.jarjestyspaikkaHierarkiaNimi) - .toLowerCase() - .includes(searchPhrase?.toLowerCase() || '') || - hakukohde.oid.includes(searchPhrase?.toLowerCase() || ''), - ); - }, [hakukohteet, searchPhrase, translateEntity]); + if (isHakukohdeOid(searchPhrase)) { + const matchingHakukohde = sortedHakukohteet.find( + (hakukohde) => hakukohde.oid === searchPhrase, + ); + return matchingHakukohde ? [matchingHakukohde] : []; + } else if (!isEmpty(searchPhraseWords)) { + return sortedHakukohteet.filter((_, index) => { + const hakukohdeTarget = hakukohdeMatchTargets[index]; + return searchPhraseWords.every((word) => + hakukohdeTarget.includes(word), + ); + }); + } else { + return sortedHakukohteet; + } + }, [ + sortedHakukohteet, + searchPhrase, + searchPhraseWords, + hakukohdeMatchTargets, + ]); return { results, diff --git a/src/app/hooks/useTranslations.ts b/src/app/hooks/useTranslations.ts index 256b10f9..bced5b44 100644 --- a/src/app/hooks/useTranslations.ts +++ b/src/app/hooks/useTranslations.ts @@ -15,7 +15,7 @@ export const useTranslations = () => { ? translateName(translateable, i18n.language as Language) : ''; }, - [i18n], + [i18n.language], ); return { t, translateEntity, language: i18n.language as Language, i18n }; diff --git a/src/app/lib/checkAccessibility.ts b/src/app/lib/checkAccessibility.ts index c71b6b6a..2f31250f 100644 --- a/src/app/lib/checkAccessibility.ts +++ b/src/app/lib/checkAccessibility.ts @@ -1,8 +1,9 @@ import React from 'react'; import { isProd, isTesting } from './configuration'; +import { isServer } from './common'; export function checkAccessibility() { - if (typeof window !== 'undefined' && !isProd && !isTesting) { + if (!isServer && !isProd && !isTesting) { Promise.all([import('@axe-core/react'), import('react-dom')]).then( ([axe, ReactDOM]) => axe.default(React, ReactDOM, 1000), ); diff --git a/src/app/lib/common.ts b/src/app/lib/common.ts index de2d8e72..e569ae51 100644 --- a/src/app/lib/common.ts +++ b/src/app/lib/common.ts @@ -66,3 +66,8 @@ export function downloadBlob(fileName: string, data: Blob) { window.URL.revokeObjectURL(url); link.remove(); } + +export const isServer = typeof window === 'undefined'; + +export const isHakukohdeOid = (value: string) => + /^1\.2\.246\.562\.20\.\d{20}$/.test(value); diff --git a/src/app/lib/configuration.ts b/src/app/lib/configuration.ts index 81a18f5f..4b5b1bd0 100644 --- a/src/app/lib/configuration.ts +++ b/src/app/lib/configuration.ts @@ -9,11 +9,16 @@ export const isProd = process.env.NODE_ENV === 'production'; export const isTesting = process.env.TEST === 'true'; +export const xstateInspect = process.env.XSTATE_INSPECT === 'true'; + type ValintatapajonoStatusParams = { valintatapajonoOid: string; status: boolean; }; +const VALINTALASKENTAKERRALLA_VANHA = + process.env.NEXT_PUBLIC_VALINTALASKENTAKERRALLA_VANHA === 'true'; + export const configuration = { // yleiset raamitUrl: `${DOMAIN}/virkailija-raamit/apply-raamit.js`, @@ -59,6 +64,9 @@ export const configuration = { // valintalaskenta-laskenta-service valintalaskentaServiceLogin: `${DOMAIN}/valintalaskenta-laskenta-service/auth/login`, valintalaskentaServiceUrl: `${DOMAIN}/valintalaskenta-laskenta-service/resources/`, + valintalaskentakerrallaUrl: VALINTALASKENTAKERRALLA_VANHA + ? `${DOMAIN}/valintalaskentakoostepalvelu/resources/valintalaskentakerralla` + : `${DOMAIN}/valintalaskenta-laskenta-service/resources/valintalaskentakerralla`, hakemuksenLasketutValinnanvaiheetUrl: ({ hakuOid, hakemusOid, diff --git a/src/app/lib/constants.ts b/src/app/lib/constants.ts index 76249cb2..3107831c 100644 --- a/src/app/lib/constants.ts +++ b/src/app/lib/constants.ts @@ -15,3 +15,5 @@ export const DEFAULT_NUQS_OPTIONS = { } as const; export const NDASH = '\u2013'; + +export const TRANSITION_DURATION = '200ms'; diff --git a/src/app/lib/http-client.ts b/src/app/lib/http-client.ts index 81580280..ae2e933e 100644 --- a/src/app/lib/http-client.ts +++ b/src/app/lib/http-client.ts @@ -1,7 +1,7 @@ import { getCookies } from './cookie'; import { redirect } from 'next/navigation'; import { configuration } from './configuration'; -import { FetchError } from './common'; +import { FetchError, isServer } from './common'; import { isPlainObject } from 'remeda'; export type HttpClientResponse = { @@ -35,7 +35,7 @@ const noContent = (response: Response) => { const redirectToLogin = () => { const loginUrl = new URL(configuration.loginUrl); loginUrl.searchParams.set('service', window.location.href); - if (typeof window === 'undefined') { + if (isServer) { redirect(loginUrl.toString()); } else { window.location.replace(loginUrl.toString()); diff --git a/src/app/lib/mui-utils.tsx b/src/app/lib/mui-utils.tsx index 027e8c43..bbe6ea12 100644 --- a/src/app/lib/mui-utils.tsx +++ b/src/app/lib/mui-utils.tsx @@ -11,9 +11,7 @@ export function withDefaultProps

>( const ComponentWithDefaultProps = forwardRef< ComponentRef>, P - >((props, ref) => ( - - )); + >((props, ref) => ); ComponentWithDefaultProps.displayName = displayName; return ComponentWithDefaultProps; diff --git a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/lib/laskenta-state.test.ts b/src/app/lib/state/laskenta-state.test.ts similarity index 66% rename from src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/lib/laskenta-state.test.ts rename to src/app/lib/state/laskenta-state.test.ts index c9ce41e9..f107c5f4 100644 --- a/src/app/haku/[oid]/hakukohde/[hakukohde]/valinnan-hallinta/lib/laskenta-state.test.ts +++ b/src/app/lib/state/laskenta-state.test.ts @@ -1,16 +1,18 @@ import { expect, test, vi, describe, afterEach, beforeEach } from 'vitest'; import { createLaskentaMachine, - LaskentaEvents, - LaskentaStates, + LaskentaEventType, + LaskentaState, StartLaskentaParams, } from './laskenta-state'; import { client } from '@/app/lib/http-client'; import { Tila } from '@/app/lib/types/kouta-types'; -import { translateName } from '@/app/lib/localization/translation-utils'; import { createActor, waitFor } from 'xstate'; +import { range } from 'remeda'; -describe('Laskenta states', async () => { +const LASKENTA_URL = 'urlmistatulosladataan'; + +describe('Laskenta state', async () => { const LASKENTAPARAMS: StartLaskentaParams = { haku: { oid: 'haku-oid', @@ -22,17 +24,18 @@ describe('Laskenta states', async () => { nimi: { fi: 'Haku' }, tila: Tila.JULKAISTU, }, - hakukohde: { - oid: 'hakukohde-oid', - hakuOid: 'haku-oid', - nimi: { fi: 'hakukohde' }, - jarjestyspaikkaHierarkiaNimi: { fi: 'Paikka' }, - organisaatioNimi: {}, - organisaatioOid: 'organisaatio-oid', - voikoHakukohteessaOllaHarkinnanvaraisestiHakeneita: false, - }, + hakukohteet: [ + { + oid: 'hakukohde-oid', + hakuOid: 'haku-oid', + nimi: { fi: 'hakukohde' }, + jarjestyspaikkaHierarkiaNimi: { fi: 'Paikka' }, + organisaatioNimi: {}, + organisaatioOid: 'organisaatio-oid', + voikoHakukohteessaOllaHarkinnanvaraisestiHakeneita: false, + }, + ], sijoitellaanko: false, - translateEntity: translateName, }; let actor = createActor(createLaskentaMachine(LASKENTAPARAMS, vi.fn())); @@ -51,15 +54,15 @@ describe('Laskenta states', async () => { buildDummyLaskentaStart(), ); vi.spyOn(client, 'get').mockImplementation(() => buildSeurantaTiedot()); - await actor.send({ type: LaskentaEvents.START }); - await actor.send({ type: LaskentaEvents.CONFIRM }); + await actor.send({ type: LaskentaEventType.START }); + actor.send({ type: LaskentaEventType.CONFIRM }); const state = await waitFor(actor, (state) => state.matches({ - [LaskentaStates.PROCESSING]: LaskentaStates.PROCESSING_WAITING, + [LaskentaState.PROCESSING]: LaskentaState.PROCESSING_WAITING, }), ); expect(state.context.laskenta.runningLaskenta?.loadingUrl).toEqual( - 'urlmistatulosladataan', + LASKENTA_URL, ); expect( state.context.laskenta.runningLaskenta?.startedNewLaskenta, @@ -74,16 +77,21 @@ describe('Laskenta states', async () => { vi.spyOn(client, 'post').mockImplementationOnce(() => buildDummyLaskentaStart(), ); - vi.spyOn(client, 'get').mockImplementation(() => - buildSeurantaTiedot(true, 1), - ); - await actor.send({ type: LaskentaEvents.START }); - await actor.send({ type: LaskentaEvents.CONFIRM }); + vi.spyOn(client, 'get').mockImplementation((url) => { + if (url.toString().includes('seuranta')) { + return buildSeurantaTiedot(true, 1); + } else if (url.toString().includes('yhteenveto')) { + return buildYhteenveto('VALMIS', 1); + } + return Promise.reject(); + }); + actor.send({ type: LaskentaEventType.START }); + actor.send({ type: LaskentaEventType.CONFIRM }); const state = await waitFor(actor, (state) => - state.matches(LaskentaStates.IDLE), + state.matches(LaskentaState.IDLE), ); expect(state.context.laskenta.runningLaskenta?.loadingUrl).toEqual( - 'urlmistatulosladataan', + LASKENTA_URL, ); expect( state.context.laskenta.runningLaskenta?.startedNewLaskenta, @@ -99,10 +107,10 @@ describe('Laskenta states', async () => { vi.spyOn(client, 'post').mockRejectedValueOnce( () => new Error('testerror'), ); - await actor.send({ type: LaskentaEvents.START }); - await actor.send({ type: LaskentaEvents.CONFIRM }); + actor.send({ type: LaskentaEventType.START }); + actor.send({ type: LaskentaEventType.CONFIRM }); const state = await waitFor(actor, (state) => - state.matches(LaskentaStates.IDLE), + state.matches(LaskentaState.IDLE), ); expect(state.context.error).toBeDefined(); }); @@ -114,13 +122,13 @@ describe('Laskenta states', async () => { vi.spyOn(client, 'get').mockImplementationOnce(() => buildSeurantaTiedot(true, 0, 1), ); - await actor.send({ type: LaskentaEvents.START }); - await actor.send({ type: LaskentaEvents.CONFIRM }); + actor.send({ type: LaskentaEventType.START }); + actor.send({ type: LaskentaEventType.CONFIRM }); const state = await waitFor(actor, (state) => - state.matches(LaskentaStates.IDLE), + state.matches(LaskentaState.IDLE), ); expect(state.context.laskenta.runningLaskenta?.loadingUrl).toEqual( - 'urlmistatulosladataan', + LASKENTA_URL, ); expect( state.context.laskenta.runningLaskenta?.startedNewLaskenta, @@ -135,7 +143,7 @@ describe('Laskenta states', async () => { const buildDummyLaskentaStart = () => { const laskenta = { - latausUrl: 'urlmistatulosladataan', + latausUrl: LASKENTA_URL, lisatiedot: { luotiinkoUusiLaskenta: true }, }; return Promise.resolve({ headers: new Headers(), data: laskenta }); @@ -154,3 +162,30 @@ const buildSeurantaTiedot = ( }; return Promise.resolve({ headers: new Headers(), data: seuranta }); }; + +const buildYhteenveto = ( + tila: 'VALMIS' | 'PERUUTETTU', + hakukohteitaValmiina = 0, + hakukohteitaKeskeytetty = 0, +) => { + const yhteenvetoValmiit = range(0, hakukohteitaValmiina).map( + (hakukohdeOid) => ({ + hakukohdeOid, + tila: 'VALMIS', + }), + ); + + const yhteenvatKeskeytetty = range(0, hakukohteitaKeskeytetty).map( + (hakukohdeOid) => ({ + hakukohdeOid, + tila: 'VIRHE', + }), + ); + + const yhteenveto = { + hakukohteet: [...yhteenvetoValmiit, ...yhteenvatKeskeytetty], + tila, + }; + + return Promise.resolve({ headers: new Headers(), data: yhteenveto }); +}; diff --git a/src/app/lib/state/laskenta-state.ts b/src/app/lib/state/laskenta-state.ts new file mode 100644 index 00000000..cb6f5377 --- /dev/null +++ b/src/app/lib/state/laskenta-state.ts @@ -0,0 +1,453 @@ +'use client'; + +import { ActorRefFrom, assign, fromPromise, setup, SnapshotFrom } from 'xstate'; +import { + Valinnanvaihe, + ValinnanvaiheTyyppi, +} from '@/app/lib/types/valintaperusteet-types'; +import { Haku, Hakukohde } from '@/app/lib/types/kouta-types'; +import { + getLaskennanSeurantaTiedot, + getLaskennanYhteenveto, + kaynnistaLaskenta, + keskeytaLaskenta, +} from '@/app/lib/valintalaskenta-service'; +import useToaster, { Toast } from '@/app/hooks/useToaster'; +import { + LaskentaSummary, + LaskentaStart, + SeurantaTiedot, + LaskentaErrorSummary, +} from '@/app/lib/types/laskenta-types'; +import { prop } from 'remeda'; +import { useMemo } from 'react'; +import { sijoitellaankoHaunHakukohteetLaskennanYhteydessa } from '@/app/lib/kouta'; +import { useMachine, useSelector } from '@xstate/react'; +import { HaunAsetukset } from '@/app/lib/types/haun-asetukset'; + +const POLLING_INTERVAL = 5000; + +export type Laskenta = { + errorMessage?: string | string[] | null; + calculatedTime?: Date | number | null; + runningLaskenta?: LaskentaStart; +}; + +const laskentaReducer = (state: Laskenta, action: Laskenta): Laskenta => { + return Object.assign({}, state, action); +}; + +export type StartLaskentaParams = { + haku: Haku; + hakukohteet: Array; + valinnanvaiheTyyppi?: ValinnanvaiheTyyppi; + sijoitellaanko: boolean; + valinnanvaiheNumber?: number; + valinnanvaiheNimi?: string; +}; + +export type LaskentaContext = { + laskenta: Laskenta; + startLaskentaParams: StartLaskentaParams; + seurantaTiedot: SeurantaTiedot | null; + errorSummary: LaskentaErrorSummary | null; + summary: LaskentaSummary | null; + error: Error | null; +}; + +export enum LaskentaState { + IDLE = 'IDLE', + INITIALIZED = 'INITIALIZED', + WAITING_CONFIRMATION = 'WAITING_CONFIRMATION', + STARTING = 'STARTING', + PROCESSING = 'PROCESSING', + PROCESSING_FETCHING = 'FETCHING', + PROCESSING_WAITING = 'WAITING', + PROCESSING_DETERMINE_POLL_COMPLETION = 'DETERMINE_POLL_COMPLETION', + FETCHING_SUMMARY = 'FETCHING_SUMMARY', + DETERMINE_SUMMARY = 'DETERMINE_SUMMARY', + // Laskennassa tai sen pyynnöissä tapahtui virhe, eikä laskenta siksi valmistunut + ERROR = 'ERROR', + // Laskenta valmistui, mutta yhteenvedossa on virheitä + COMPLETED_WITH_ERRORS = 'COMPLETED_WITH_ERRORS', + COMPLETED = 'COMPLETED', + CANCELING = 'CANCELING', +} + +export enum LaskentaEventType { + START = 'START', + CONFIRM = 'CONFIRM', + CANCEL = 'CANCEL', + RESET_RESULTS = 'RESET_RESULTS', +} + +export type LaskentaEvent = { + type: LaskentaEventType; +}; + +export type LaskentaStateTags = + | 'stopped' + | 'started' + | 'completed' + | 'canceling'; + +export type LaskentaMachineSnapshot = SnapshotFrom< + ReturnType +>; + +export type LaskentaActorRef = ActorRefFrom< + ReturnType +>; + +export const createLaskentaMachine = ( + params: StartLaskentaParams, + addToast: (toast: Toast) => void, +) => { + const valinnanvaiheSelected: boolean = Boolean(params.valinnanvaiheNimi); + const keyPartValinnanvaihe = valinnanvaiheSelected + ? `-valinnanvaihe_${params.valinnanvaiheNumber ?? 0}` + : ''; + + const initialContext = { + laskenta: {}, + startLaskentaParams: params, + seurantaTiedot: null, + // Laskennan yhteenveto + summary: null, + // Laskennan yhteenvedon virheilmoitukset + errorSummary: null, + // Mahdollinen virheolio. Huom! errorSummary voi sisältää jotain, vaikka error on null + error: null, + }; + + const machineKey = `haku_${params.haku.oid}-hakukohteet_${params.hakukohteet.map(prop('oid')).join('_')}${keyPartValinnanvaihe}`; + return setup({ + types: { + context: {} as LaskentaContext, + tags: 'stopped' as LaskentaStateTags, + }, + actors: { + startLaskenta: fromPromise( + ({ input }: { input: StartLaskentaParams }) => { + return kaynnistaLaskenta({ + haku: input.haku, + hakukohteet: input.hakukohteet, + valinnanvaiheTyyppi: input.valinnanvaiheTyyppi, + sijoitellaankoHaunHakukohteetLaskennanYhteydessa: + input.sijoitellaanko, + valinnanvaihe: input.valinnanvaiheNumber, + }); + }, + ), + pollLaskenta: fromPromise(({ input }: { input: Laskenta }) => { + if (input.runningLaskenta) { + return getLaskennanSeurantaTiedot(input.runningLaskenta.loadingUrl); + } + return Promise.reject( + Error( + 'Tried to fetch seurantatiedot without having access to started laskenta', + ), + ); + }), + fetchSummary: fromPromise(({ input }: { input: Laskenta }) => { + if (input.runningLaskenta) { + return getLaskennanYhteenveto(input.runningLaskenta.loadingUrl); + } + return Promise.reject( + Error('Tried to fetch summary without having access to laskenta'), + ); + }), + stopLaskenta: fromPromise(({ input }: { input: Laskenta }) => { + if (input.runningLaskenta) { + return keskeytaLaskenta({ + laskentaUuid: input.runningLaskenta.loadingUrl, + }); + } + return Promise.reject( + Error( + 'Failed to stop laskenta without having access to started laskenta', + ), + ); + }), + }, + }).createMachine({ + /** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOgEkARAGQFEBiAZQBUBBAJSYG0AGAXUVAAHAPaxcAF1zD8AkAA9EAJgAcAThLcAjMoDsOgMyrlANlUAWY4oA0IAJ6JlmksZ0BWbtz1HNZ1ToC+-jZoWHiEpJS0dGw0DDRMAPoxDACqVEwMPPxIICJiktKyCgiK+prGJGaamqXcpWZm3GbWdg56JNW6nq6uVWb6AUEgITgExCQA6ixkTGQAcgDiCQDCAPJzAGJkbACyLLPrdGub2ztZsnkSUjI5xe7cJPrKPWbursYNOsY29iUGJG5XMpFFpuMoLHpAsEMKNwpNprNFit1ltdvsyIdliw5ssaFRzjlLgUbqBiopVOotLoDEZTBYWr8QU5jNxLCzNDpFDojGYocMYWFxsx2IiFnQINIwCQCAA3YQAaylI0FpGFHHmCwQsuEmHQxKyBKEoiuhVuiE0TRIvX0LKqbJcph+iEaD1M3AGik9WlcnOMfOVY1VrHVizoYAATuHhOGSIIADZ6gBm0dQJADcLVoq1+Dluv1fENuWNxKKiFUmlcVq5ilc5Lq+jMaidCEMZitJkUDtMNeB-oFgZIAAU2KtcQwGBqjtjcfi+Bdi9dSwgatxK8Ynj73eCa4ZmxaayQeToah8bTplH3QgPh6PYhPFiQNvFlgAJScSwjSnMKpX9uE3sd7wWR9nzfRZs1zPVrgNOdCQXU1SSUIFKh9ZRgRtVlgWeZtlH0B4wW5HRPFZTkykvWFxgAu8NRAphX0nCMoxjeMkxTNM-0okdAJop86LAzVtTzaCC1go18kXM0EFUHoOn0fQgS5b0bVUZtVBtAE0OaYEvk8MihnTTjb3HGiphmSc5FgcQ9SldBE3ECNkHubgiDoAzSCo4yH1M0VCyJCTEKk5R8LUyxqk0Pw1E0ZtXHMSpG2PMFVG4IxcPIlUhy46iHwoeIaF2eYaASQdVioKhkR2QdaAOOY6F8+CSXkc1VzbHddBBDwXFcHDXH0AFmmk9c6ndJo0uvTLPOAnKmDynYCqKkqyrWCqqoxGrOE0bIxJNBrigtRoOlw3CLV6d5m3PdQgSIrQa20VQXFG-9xqAkgsRxPF30lL85UVdir0eozntemcNQgnUoOkGDNqLcSEMals8IBLk3HPMFFJ6M6GitVQVAGCtzoGB7DO4h8gfe0NGOjWME3EZNw1TNyMoBmjSaoEHBPB-BIfnGGdsQbkAWMFkBjkmoDGaM6tD6tCmkcO7Bb9fSONIXj6KRVIdj2NgAE1xU+7UfoZlX+ISdXNa10GhIhkSob82HdvcCpPG0clwRZZ4GUQGL1Du7RwvC1xNAGQZoT+8YjY1E2Ug19gdYp5jqdp+mldo1WllNmOLY5rm4J5pcOQDkhtzBVkzE5VcPYQYE23XND3AaRwykUQnSCmma5vT7XatE6HtqXJlHeMcEMItD5+mbSxeptcL+mqIO9JDiiW9y-K5kKjudfWm36r7uonB9npdAsVkutaBAel6+KA7wqowt5Pl8GECA4FkNzud7ySAFpyUUSpgV6PRnhERiifX4H9Kx6F8Dofo3QYouDvgvdKkQaBvxLJJL0FRfbYyMPoRQvhpLNjkpPC0hhOTEMFk3RWocIjUBoLGcMYAZRSAAK7wBzu-AKrUBaC16O1eS3xT7NAeDpAYalygwIVgggcSDyBzDMiwVmAAtGgFAUH+ThjdXqLsmhyR6GyZQZ0uTOGMDUNqUC-CrmbuQGhJA8ojjYKou2SgaxtkwS7HBeCQFllLhpL4GFjy+goZIuE0ilqVVyhQBIEwZgvgSLY1YbAGAON5iUBolZS4Wj2v0IE-DfiOAwRFbGNZkpe0sSE1Yy1wlJL7gHB4jZro9HCjgqB0UfDODcB4eS7g-6BP5FQ+EZkkTHFRHsaqVTJINkLlyYx3IBrkkHs2ckTg6jlgPsYc+npLGZg1GMgKDQKhAkEc1QeO4zAEKMFaPCTwyimDMfA3pi9GbEwWDsuGX9MaNmcQAzcwDoqskPEYTkuh3bnUsR5Z64dFgvOKB-cKFQPn-3PN8-Bp9uQ-05CoFkgscHKVBU9EyCJtlsNQQFPa3stzNDKLhEK49eiPCdgMd4OCG64qZtlZes1V7zVKuVMJoyiVqOKL4B4FYqjvCSuuD0OFXiHh6iYF0-8TAsqeS9acZNnn8scS2B4FIcHJRqFg7+e4qQAmku8TsuhyzgksRCtOUczZQvNBYXquEgS1mxslNC+jT7aUPFyaSR9cKvH0JY1uK8152pjg65cbheoBxMOCQOfg9AV3XG2XQ65EVzI5IEQIQA */ + id: `LaskentaMachine-${machineKey}`, + initial: LaskentaState.IDLE, + context: initialContext, + states: { + [LaskentaState.IDLE]: { + tags: ['stopped'], + initial: LaskentaState.INITIALIZED, + on: { + [LaskentaEventType.START]: { + target: LaskentaState.WAITING_CONFIRMATION, + }, + [LaskentaEventType.RESET_RESULTS]: { + target: '#INITIALIZED', + }, + }, + states: { + previous: { type: 'history' }, + [LaskentaState.INITIALIZED]: { + id: LaskentaState.INITIALIZED, + actions: assign(initialContext), + }, + [LaskentaState.ERROR]: { + id: LaskentaState.ERROR, + }, + [LaskentaState.COMPLETED_WITH_ERRORS]: { + id: LaskentaState.COMPLETED_WITH_ERRORS, + tags: ['completed'], + }, + [LaskentaState.COMPLETED]: { + id: LaskentaState.COMPLETED, + tags: ['completed'], + entry: [ + assign({ + laskenta: ({ context }) => + laskentaReducer(context.laskenta, { + calculatedTime: new Date(), + }), + }), + ({ context }) => { + const key = `laskenta-completed-for-${machineKey}`; + const message = valinnanvaiheSelected + ? 'valinnanhallinta.valmisvalinnanvaihe' + : 'valinnanhallinta.valmis'; + const messageParams = valinnanvaiheSelected + ? { + nimi: context.startLaskentaParams.valinnanvaiheNimi ?? '', + } + : undefined; + addToast({ key, message, type: 'success', messageParams }); + }, + ], + }, + }, + }, + [LaskentaState.WAITING_CONFIRMATION]: { + tags: ['stopped'], + on: { + [LaskentaEventType.CONFIRM]: { + target: LaskentaState.STARTING, + actions: assign(initialContext), + }, + [LaskentaEventType.CANCEL]: { + target: 'IDLE.previous', + }, + }, + }, + [LaskentaState.STARTING]: { + tags: ['started'], + invoke: { + src: 'startLaskenta', + input: ({ context }) => context.startLaskentaParams, + onDone: { + target: LaskentaState.PROCESSING, + actions: assign({ + laskenta: ({ event, context }) => + laskentaReducer(context.laskenta, { + runningLaskenta: event.output, + }), + }), + }, + onError: { + target: '#ERROR', + actions: assign({ + error: ({ event }) => event.error as Error, + }), + }, + }, + }, + [LaskentaState.PROCESSING]: { + tags: ['started'], + initial: LaskentaState.PROCESSING_FETCHING, + on: { + [LaskentaEventType.CANCEL]: '#CANCELING', + }, + states: { + [LaskentaState.PROCESSING_FETCHING]: { + invoke: { + src: 'pollLaskenta', + input: ({ context }) => context.laskenta, + onDone: { + target: LaskentaState.PROCESSING_DETERMINE_POLL_COMPLETION, + actions: assign({ + seurantaTiedot: ({ event }) => event.output, + }), + }, + onError: { + target: '#ERROR', + actions: assign({ + error: ({ event }) => event.error as Error, + }), + }, + }, + }, + [LaskentaState.PROCESSING_WAITING]: { + after: { + [POLLING_INTERVAL]: LaskentaState.PROCESSING_FETCHING, + }, + }, + [LaskentaState.PROCESSING_DETERMINE_POLL_COMPLETION]: { + always: [ + { + guard: ({ context }) => + context.seurantaTiedot?.tila === 'VALMIS' || + context.seurantaTiedot?.tila === 'PERUUTETTU', + target: '#FETCHING_SUMMARY', + }, + { + target: LaskentaState.PROCESSING_WAITING, + }, + ], + }, + [LaskentaState.CANCELING]: { + id: LaskentaState.CANCELING, + tags: ['canceling'], + invoke: { + src: 'stopLaskenta', + input: ({ context }) => context.laskenta, + onDone: { + target: '#FETCHING_SUMMARY', + }, + onError: { + target: '#ERROR', + }, + }, + }, + }, + }, + [LaskentaState.FETCHING_SUMMARY]: { + tags: ['started'], + id: LaskentaState.FETCHING_SUMMARY, + invoke: { + src: 'fetchSummary', + input: ({ context }) => context.laskenta, + onDone: { + target: LaskentaState.DETERMINE_SUMMARY, + actions: assign({ + summary: ({ event }) => event.output, + // TODO: Poista errorSummary, kun virheiden esittäminen on yhdenmukaistettu myös "valinnan hallinta"-näkymässä + errorSummary: ({ event }) => + event.output?.hakukohteet + ?.filter((hk) => + hk.ilmoitukset?.some((i) => i.tyyppi === 'VIRHE'), + ) + .map((hakukohde) => { + return { + hakukohdeOid: hakukohde.hakukohdeOid, + notifications: hakukohde.ilmoitukset?.map( + (i) => i.otsikko, + ), + }; + })[0], + seurantaTiedot: ({ event, context }) => { + const hakukohteitaYhteensa = + context.seurantaTiedot?.hakukohteitaYhteensa ?? 0; + + const hakukohteitaValmiina = + event.output?.hakukohteet?.filter( + (hk) => hk.tila === 'VALMIS', + )?.length ?? 0; + const hakukohteitaKeskeytetty = + hakukohteitaYhteensa - hakukohteitaValmiina; + const tila = event.output.tila ?? context?.seurantaTiedot?.tila; + + return context.seurantaTiedot + ? { + ...context.seurantaTiedot, + tila, + hakukohteitaValmiina: + hakukohteitaYhteensa - hakukohteitaKeskeytetty, + hakukohteitaKeskeytetty, + } + : context.seurantaTiedot; + }, + }), + }, + onError: { + target: '#ERROR', + actions: assign({ + error: ({ event }) => event.error as Error, + }), + }, + }, + }, + [LaskentaState.DETERMINE_SUMMARY]: { + tags: ['started'], + always: [ + { + guard: ({ context }) => + (context.seurantaTiedot != null && + context.seurantaTiedot.hakukohteitaKeskeytetty > 0) || + (context.errorSummary?.notifications?.length ?? 0) > 0, + target: '#COMPLETED_WITH_ERRORS', + actions: assign({ + laskenta: ({ context }) => + laskentaReducer(context.laskenta, { + errorMessage: context.errorSummary?.notifications, + }), + }), + }, + { + target: '#COMPLETED', + }, + ], + }, + }, + }); +}; + +type LaskentaStateParams = { + haku: Haku; + haunAsetukset: HaunAsetukset; + hakukohteet: Hakukohde | Array; + vaihe?: Valinnanvaihe; + valinnanvaiheNumber?: number; + addToast: (toast: Toast) => void; +}; + +export const useLaskentaState = ({ + haku, + haunAsetukset, + hakukohteet, + vaihe, + valinnanvaiheNumber, +}: LaskentaStateParams) => { + const { addToast } = useToaster(); + + const laskentaMachine = useMemo(() => { + return createLaskentaMachine( + { + haku, + hakukohteet: Array.isArray(hakukohteet) ? hakukohteet : [hakukohteet], + sijoitellaanko: sijoitellaankoHaunHakukohteetLaskennanYhteydessa( + haku, + haunAsetukset, + ), + ...(vaihe && valinnanvaiheNumber + ? { + valinnanvaiheTyyppi: vaihe.tyyppi, + valinnanvaiheNumber, + valinnanvaiheNimi: vaihe.nimi, + } + : {}), + }, + addToast, + ); + }, [haku, hakukohteet, haunAsetukset, vaihe, valinnanvaiheNumber, addToast]); + + return useMachine(laskentaMachine); +}; + +export const useLaskentaError = (actorRef: LaskentaActorRef) => { + const error = useSelector(actorRef, (state) => state.context.error); + const laskenta = useSelector(actorRef, (state) => state.context.laskenta); + const hasError = laskenta.errorMessage != null || error; + + return hasError + ? (laskenta.errorMessage ?? '' + (error?.message ?? error)) + : ''; +}; diff --git a/src/app/lib/theme.tsx b/src/app/lib/theme.tsx index 0e93d170..26850b9f 100644 --- a/src/app/lib/theme.tsx +++ b/src/app/lib/theme.tsx @@ -27,15 +27,6 @@ export const notLarge = (theme: Theme) => theme.breakpoints.down('lg'); export const THEME_OVERRIDES: ThemeOptions = { components: { - MuiInputBase: { - styleOverrides: { - root: { - borderColor: ophColors.grey800, - borderRadius: '2px', - height: '48px', - }, - }, - }, MuiDialog: { defaultProps: { fullWidth: true, diff --git a/src/app/lib/types/laskenta-types.ts b/src/app/lib/types/laskenta-types.ts index 46d78737..4824e933 100644 --- a/src/app/lib/types/laskenta-types.ts +++ b/src/app/lib/types/laskenta-types.ts @@ -66,10 +66,11 @@ export type LaskettuValinnanVaiheModel = { }; export type SeurantaTiedot = { - tila: 'VALMIS' | 'MENEILLAAN'; + tila: 'VALMIS' | 'MENEILLAAN' | 'ALOITTAMATTA' | 'PERUUTETTU'; hakukohteitaYhteensa: number; hakukohteitaValmiina: number; hakukohteitaKeskeytetty: number; + jonosija: number | null; }; export type LaskentaStart = { @@ -79,7 +80,19 @@ export type LaskentaStart = { export type LaskentaErrorSummary = { hakukohdeOid: string; - notifications: string[] | undefined; + notifications: Array | undefined; +}; + +export type LaskentaSummary = { + tila: 'PERUUTETTU' | 'VALMIS'; + hakukohteet: Array<{ + hakukohdeOid: string; + tila: 'TEKEMATTA' | 'VALMIS' | 'VIRHE'; + ilmoitukset: Array<{ otsikko: string; tyyppi: string }>; + }>; + ilmoitus?: { + otsikko: string; + }; }; export enum ValintakoeOsallistuminenTulos { diff --git a/src/app/lib/valintalaskenta-service.ts b/src/app/lib/valintalaskenta-service.ts index 0f831a1e..72da8abc 100644 --- a/src/app/lib/valintalaskenta-service.ts +++ b/src/app/lib/valintalaskenta-service.ts @@ -11,7 +11,7 @@ import { HakijaryhmanHakija, HakukohteenHakijaryhma, JarjestyskriteeriTila, - LaskentaErrorSummary, + LaskentaSummary, LaskentaStart, LaskettuValinnanVaiheModel, SeurantaTiedot, @@ -38,9 +38,9 @@ import { HarkinnanvaraisestiHyvaksytty, } from './types/harkinnanvaraiset-types'; import { queryOptions } from '@tanstack/react-query'; -import { TranslatedName } from './localization/localization-types'; import { getFullnameOfHakukohde, Haku, Hakukohde } from './types/kouta-types'; import { ValinnanvaiheTyyppi } from './types/valintaperusteet-types'; +import { translateName } from './localization/translation-utils'; const formSearchParamsForStartLaskenta = ({ laskentaUrl, @@ -49,24 +49,22 @@ const formSearchParamsForStartLaskenta = ({ valinnanvaiheTyyppi, sijoitellaankoHaunHakukohteetLaskennanYhteydessa, valinnanvaihe, - translateEntity, }: { laskentaUrl: URL; haku: Haku; - hakukohde: Hakukohde; + hakukohde?: Hakukohde; valinnanvaiheTyyppi?: ValinnanvaiheTyyppi; sijoitellaankoHaunHakukohteetLaskennanYhteydessa: boolean; valinnanvaihe?: number; - translateEntity: (translateable: TranslatedName) => string; }): URL => { laskentaUrl.searchParams.append( 'erillishaku', '' + sijoitellaankoHaunHakukohteetLaskennanYhteydessa, ); - laskentaUrl.searchParams.append('haunnimi', translateEntity(haku.nimi)); + laskentaUrl.searchParams.append('haunnimi', translateName(haku.nimi)); laskentaUrl.searchParams.append( 'nimi', - getFullnameOfHakukohde(hakukohde, translateEntity), + hakukohde ? getFullnameOfHakukohde(hakukohde, translateName) : '', ); if (valinnanvaihe && valinnanvaiheTyyppi !== ValinnanvaiheTyyppi.VALINTAKOE) { laskentaUrl.searchParams.append('valinnanvaihe', '' + valinnanvaihe); @@ -81,34 +79,39 @@ const formSearchParamsForStartLaskenta = ({ }; type LaskentaStatusResponseData = { + latausUrl: string; lisatiedot: { luotiinkoUusiLaskenta: boolean; }; - latausUrl: string; }; -export const kaynnistaLaskenta = async ( - haku: Haku, - hakukohde: Hakukohde, - valinnanvaiheTyyppi: ValinnanvaiheTyyppi, - sijoitellaankoHaunHakukohteetLaskennanYhteydessa: boolean, - valinnanvaihe: number, - translateEntity: (translateable: TranslatedName) => string, -): Promise => { +export const kaynnistaLaskenta = async ({ + haku, + hakukohteet, + valinnanvaiheTyyppi, + sijoitellaankoHaunHakukohteetLaskennanYhteydessa, + valinnanvaihe, +}: { + haku: Haku; + hakukohteet: Array; + valinnanvaiheTyyppi?: ValinnanvaiheTyyppi; + sijoitellaankoHaunHakukohteetLaskennanYhteydessa: boolean; + valinnanvaihe?: number; +}): Promise => { + const singleHakukohde = hakukohteet.length === 1 ? hakukohteet[0] : undefined; const laskentaUrl = formSearchParamsForStartLaskenta({ laskentaUrl: new URL( - `${configuration.valintalaskentaServiceUrl}valintalaskentakerralla/haku/${haku.oid}/tyyppi/HAKUKOHDE/whitelist/true?`, + `${configuration.valintalaskentakerrallaUrl}/haku/${haku.oid}/tyyppi/HAKUKOHDE/whitelist/true?`, ), haku, - hakukohde, + hakukohde: singleHakukohde, valinnanvaiheTyyppi: valinnanvaiheTyyppi, sijoitellaankoHaunHakukohteetLaskennanYhteydessa, valinnanvaihe, - translateEntity, }); const response = await client.post( laskentaUrl.toString(), - [hakukohde.oid], + hakukohteet.map(prop('oid')), ); return { startedNewLaskenta: response.data?.lisatiedot?.luotiinkoUusiLaskenta, @@ -116,55 +119,23 @@ export const kaynnistaLaskenta = async ( }; }; -export const kaynnistaLaskentaHakukohteenValinnanvaiheille = async ( - haku: Haku, - hakukohde: Hakukohde, - sijoitellaankoHaunHakukohteetLaskennanYhteydessa: boolean, - translateEntity: (translateable: TranslatedName) => string, -): Promise => { - const laskentaUrl = formSearchParamsForStartLaskenta({ - laskentaUrl: new URL( - `${configuration.valintalaskentaServiceUrl}valintalaskentakerralla/haku/${haku.oid}/tyyppi/HAKUKOHDE/whitelist/true?`, - ), - haku, - hakukohde, - sijoitellaankoHaunHakukohteetLaskennanYhteydessa, - translateEntity, - }); - const response = await client.post( - laskentaUrl.toString(), - [hakukohde.oid], +export const keskeytaLaskenta = async ({ + laskentaUuid, +}: { + laskentaUuid: string; +}): Promise => { + await client.delete( + `${configuration.valintalaskentakerrallaUrl}/haku/${laskentaUuid}`, ); - return { - startedNewLaskenta: response.data?.lisatiedot?.luotiinkoUusiLaskenta, - loadingUrl: response.data?.latausUrl, - }; }; -export const getLaskennanTilaHakukohteelle = async ( +export const getLaskennanYhteenveto = async ( loadingUrl: string, -): Promise => { - const response = await client.get<{ - hakukohteet: Array<{ - hakukohdeOid: string; - ilmoitukset: [{ otsikko: string; tyyppi: string }] | null; - }>; - }>( - `${configuration.valintalaskentaServiceUrl}valintalaskentakerralla/status/${loadingUrl}/yhteenveto`, +): Promise => { + const response = await client.get( + `${configuration.valintalaskentakerrallaUrl}/status/${loadingUrl}/yhteenveto`, ); - return response.data?.hakukohteet - ?.filter((hk) => hk.ilmoitukset?.some((i) => i.tyyppi === 'VIRHE')) - .map( - (hakukohde: { - hakukohdeOid: string; - ilmoitukset: [{ otsikko: string }] | null; - }) => { - return { - hakukohdeOid: hakukohde.hakukohdeOid, - notifications: hakukohde.ilmoitukset?.map((i) => i.otsikko), - }; - }, - )[0]; + return response.data; }; export const getHakukohteenLasketutValinnanvaiheet = async ( @@ -236,6 +207,7 @@ export const getLaskennanSeurantaTiedot = async (loadingUrl: string) => { hakukohteitaYhteensa: response.data?.hakukohteitaYhteensa, hakukohteitaValmiina: response.data?.hakukohteitaValmiina, hakukohteitaKeskeytetty: response.data?.hakukohteitaKeskeytetty, + jonosija: response.data?.jonosija, }; }; diff --git a/src/app/lib/valintaperusteet.test.ts b/src/app/lib/valintaperusteet.test.ts index 279770a0..65101ae8 100644 --- a/src/app/lib/valintaperusteet.test.ts +++ b/src/app/lib/valintaperusteet.test.ts @@ -88,7 +88,7 @@ test('laskenta is not used for valinnanvaihe when jonos are not using laskenta', expect(isLaskentaUsedForValinnanvaihe(vaihe)).toBeFalsy(); }); -test('laskenta is not used for valinnanvaihe when jonos best before date has passed ', () => { +test('laskenta is not used for valinnanvaihe when jonos best before date has passed', () => { const vaihe: Valinnanvaihe = { aktiivinen: true, jonot: [ diff --git a/src/app/lib/xstate-utils.ts b/src/app/lib/xstate-utils.ts new file mode 100644 index 00000000..35f75f64 --- /dev/null +++ b/src/app/lib/xstate-utils.ts @@ -0,0 +1,19 @@ +import { useMachine } from '@xstate/react'; + +import { createBrowserInspector } from '@statelyai/inspect'; +import { isServer } from '@tanstack/react-query'; +import { xstateInspect } from './configuration'; + +type UseMachineParams = Parameters; + +const inspect = + xstateInspect && isServer + ? undefined + : (createBrowserInspector()?.inspect ?? undefined); + +export const useXstateMachine = ( + machine: UseMachineParams[0], + options: UseMachineParams[1] = {}, +) => { + return useMachine(machine, { inspect, ...options }); +}; diff --git a/src/app/lokalisaatio/fi.json b/src/app/lokalisaatio/fi.json index bfb06607..c5ac2364 100644 --- a/src/app/lokalisaatio/fi.json +++ b/src/app/lokalisaatio/fi.json @@ -410,6 +410,21 @@ "valintalaskenta-save-success": "Valintalaskennan tietojen tallentaminen onnistui", "valintalaskenta-delete-error": "Valintalaskennan muokkauksen poistaminen epäonnistui", "valintalaskenta-delete-success": "Valintalaskennan muokkauksen poistaminen onnistui", - "pistesyotto": "Pistesyöttö" + "pistesyotto": "Pistesyöttö", + "suorita-valintalaskenta": "Suorita valintalaskenta", + "keskeyta-valintalaskenta": "Keskeytä valintalaskenta", + "valintalaskenta": "Valintalaskenta", + "sulje-laskennan-tiedot": "Sulje laskennan tiedot", + "piilota-suorittamattomat-hakukohteet": "Piilota suorittamattomat hakukohteet", + "nayta-suorittamattomat-hakukohteet": "Näytä suorittamattomat hakukohteet", + "syy": "Syy", + "valintalaskenta-epaonnistui": "Valintalaskenta epäonnistui", + "hakukohteita-valmiina": "Hakukohteita valmiina", + "suorittamattomia-hakukohteita": "Suorittamattomia hakukohteita", + "keskeytetaan-laskentaa": "Keskeytetään laskentaa...", + "kaynnistetaan-laskentaa": "Käynnistetään laskentaa...", + "tehtava-on-laskennassa-jonosijalla": "Tehtävä on laskennassa jonosijalla", + "tehtava-on-laskennassa-parhaillaan": "Tehtävä on laskennassa parhaillaan", + "laskenta-on-paattynyt": "Laskenta on päättynyt" } } diff --git a/tests/e2e/hakukohde-tabs.spec.ts b/tests/e2e/hakukohde-tabs.spec.ts index 4d48fd85..d441fd41 100644 --- a/tests/e2e/hakukohde-tabs.spec.ts +++ b/tests/e2e/hakukohde-tabs.spec.ts @@ -56,10 +56,10 @@ test('navigates to hakukohde tabs', async ({ page }) => { await expect(hakukohdeNavItems).toHaveCount(3); await hakukohdeNavItems.first().click(); await expect(page.locator('span.organisaatioLabel')).toHaveText( - 'Tampereen yliopisto, Rakennetun ympäristön tiedekunta', + 'Tampereen yliopisto, Tekniikan ja luonnontieteiden tiedekunta', ); await expect(page.locator('span.hakukohdeLabel')).toHaveText( - 'Finnish MAOL competition route, Technology, Sustainable Urban Development, Bachelor and Master of Science (Technology) (3 + 2 yrs)', + 'Finnish MAOL competition route, Computing and Electrical Engineering, Science and Engineering, Bachelor and Master of Science (Technology) (3 + 2 yrs)', ); await expect(page.locator('h3')).toHaveText('Valintatapajonot'); }); diff --git a/tests/e2e/henkilo.spec.ts b/tests/e2e/henkilo.spec.ts index e4583435..b6fa8eee 100644 --- a/tests/e2e/henkilo.spec.ts +++ b/tests/e2e/henkilo.spec.ts @@ -24,10 +24,11 @@ import { forEachObj, isShallowEqual } from 'remeda'; import { NDASH } from '@/app/lib/constants'; const HAKUKOHDE_OID = '1.2.246.562.20.00000000000000045105'; +const NUKETTAJA_HAKEMUS_OID = '1.2.246.562.11.00000000000001796027'; const VALINNAN_TULOS_RESULT = hakemusValinnanTulosFixture({ hakukohdeOid: HAKUKOHDE_OID, - hakemusOid: '1.2.246.562.11.00000000000001796027', + hakemusOid: NUKETTAJA_HAKEMUS_OID, valintatapajonoOid: '17093042998533736417074016063604', henkiloOid: '1.2.246.562.24.69259807406', valinnantila: SijoittelunTila.HYVAKSYTTY, @@ -50,15 +51,13 @@ test.beforeEach(async ({ page }) => { await page.route(configuration.hakemuksetUrl, (route) => { return route.fulfill({ - json: HAKENEET.filter( - ({ oid }) => oid === '1.2.246.562.11.00000000000001793706', - ), + json: HAKENEET.filter(({ oid }) => oid === NUKETTAJA_HAKEMUS_OID), }); }); await page.route( configuration.hakemuksenLasketutValinnanvaiheetUrl({ hakuOid: '1.2.246.562.29.00000000000000045102', - hakemusOid: '1.2.246.562.11.00000000000001796027', + hakemusOid: NUKETTAJA_HAKEMUS_OID, }), (route) => route.fulfill({ @@ -68,7 +67,7 @@ test.beforeEach(async ({ page }) => { await page.route( configuration.hakemuksenSijoitteluajonTuloksetUrl({ hakuOid: '1.2.246.562.29.00000000000000045102', - hakemusOid: '1.2.246.562.11.00000000000001796027', + hakemusOid: NUKETTAJA_HAKEMUS_OID, }), (route) => { return route.fulfill({ @@ -79,7 +78,7 @@ test.beforeEach(async ({ page }) => { await page.route( configuration.hakemuksenValinnanTulosUrl({ - hakemusOid: '1.2.246.562.11.00000000000001796027', + hakemusOid: NUKETTAJA_HAKEMUS_OID, }), (route) => { return route.fulfill({ @@ -203,7 +202,7 @@ test('Displays selected henkilö info with hakutoive but without valintalaskenta await page.route( configuration.hakemuksenLasketutValinnanvaiheetUrl({ hakuOid: '1.2.246.562.29.00000000000000045102', - hakemusOid: '1.2.246.562.11.00000000000001796027', + hakemusOid: NUKETTAJA_HAKEMUS_OID, }), (route) => route.fulfill({ @@ -216,7 +215,7 @@ test('Displays selected henkilö info with hakutoive but without valintalaskenta await page.route( configuration.hakemuksenSijoitteluajonTuloksetUrl({ hakuOid: '1.2.246.562.29.00000000000000045102', - hakemusOid: '1.2.246.562.11.00000000000001796027', + hakemusOid: NUKETTAJA_HAKEMUS_OID, }), (route) => { return route.fulfill({ @@ -227,7 +226,7 @@ test('Displays selected henkilö info with hakutoive but without valintalaskenta await page.route( configuration.hakemuksenValinnanTulosUrl({ - hakemusOid: '1.2.246.562.11.00000000000001796027', + hakemusOid: NUKETTAJA_HAKEMUS_OID, }), (route) => { return route.fulfill({ @@ -325,7 +324,7 @@ test('Displays selected henkilö hakutoiveet with laskenta results only', async await page.route( configuration.hakemuksenSijoitteluajonTuloksetUrl({ hakuOid: '1.2.246.562.29.00000000000000045102', - hakemusOid: '1.2.246.562.11.00000000000001796027', + hakemusOid: NUKETTAJA_HAKEMUS_OID, }), (route) => { return route.fulfill({ @@ -336,7 +335,7 @@ test('Displays selected henkilö hakutoiveet with laskenta results only', async await page.route( configuration.hakemuksenValinnanTulosUrl({ - hakemusOid: '1.2.246.562.11.00000000000001796027', + hakemusOid: NUKETTAJA_HAKEMUS_OID, }), (route) => { return route.fulfill({ @@ -451,7 +450,7 @@ test('Sends valintalaskenta save request with right values and shows success not page, }) => { const muokkausUrl = configuration.jarjestyskriteeriMuokkausUrl({ - hakemusOid: '1.2.246.562.11.00000000000001796027', + hakemusOid: NUKETTAJA_HAKEMUS_OID, valintatapajonoOid: '17093042998533736417074016063604', jarjestyskriteeriPrioriteetti: 0, }); @@ -500,7 +499,7 @@ test('Sends valintalaskenta save request with right values and shows success not test('Show notification on valintalaskenta save error', async ({ page }) => { await page.route( configuration.jarjestyskriteeriMuokkausUrl({ - hakemusOid: '1.2.246.562.11.00000000000001796027', + hakemusOid: NUKETTAJA_HAKEMUS_OID, valintatapajonoOid: '17093042998533736417074016063604', jarjestyskriteeriPrioriteetti: 0, }), @@ -636,149 +635,345 @@ test('Show notification on valinta save error', async ({ page }) => { ).toBeVisible(); }); -test('Displays pistesyöttö', async ({ page }) => { - await page.goto( - '/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000045102/henkilo/1.2.246.562.11.00000000000001796027', - ); +test.describe('Pistesyöttö', () => { + test('Displays pistesyöttö', async ({ page }) => { + await page.goto( + '/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000045102/henkilo/1.2.246.562.11.00000000000001796027', + ); - await expectAllSpinnersHidden(page); + await expectAllSpinnersHidden(page); - const pistesyottoHeading = page.getByRole('heading', { name: 'Pistesyöttö' }); - await expect(pistesyottoHeading).toBeVisible(); + const pistesyottoHeading = page.getByRole('heading', { + name: 'Pistesyöttö', + }); + await expect(pistesyottoHeading).toBeVisible(); - const nakkikoe = page.getByRole('region', { - name: 'Nakkikoe, oletko nakkisuojassa?', - }); + const nakkikoe = page.getByRole('region', { + name: 'Nakkikoe, oletko nakkisuojassa?', + }); - await expect(nakkikoe).toBeVisible(); + await expect(nakkikoe).toBeVisible(); - const nakkiKyllaInput = nakkikoe.getByText('Kyllä'); - await expect(nakkiKyllaInput).toBeVisible(); - const nakkiOsallistuiInput = nakkikoe.getByText('Osallistui', { - exact: true, - }); - await expect(nakkiOsallistuiInput).toBeVisible(); + const nakkiKyllaInput = nakkikoe.getByText('Kyllä'); + await expect(nakkiKyllaInput).toBeVisible(); + const nakkiOsallistuiInput = nakkikoe.getByText('Osallistui', { + exact: true, + }); + await expect(nakkiOsallistuiInput).toBeVisible(); - const nakkiTallennaButton = nakkikoe.getByRole('button', { - name: 'Tallenna', - }); + const nakkiTallennaButton = nakkikoe.getByRole('button', { + name: 'Tallenna', + }); - await expect(nakkiTallennaButton).toBeEnabled(); + await expect(nakkiTallennaButton).toBeEnabled(); - const koksakoe = page.getByRole('region', { - name: `Köksäkokeen arvosana 4${NDASH}10`, - }); + const koksakoe = page.getByRole('region', { + name: `Köksäkokeen arvosana 4${NDASH}10`, + }); - await expect(koksakoe).toBeVisible(); + await expect(koksakoe).toBeVisible(); - const koksaPisteetInput = koksakoe.getByLabel('Pisteet'); - await expect(koksaPisteetInput).toBeVisible(); - const koksaOsallistuiInput = koksakoe.getByText('Osallistui', { - exact: true, - }); - await expect(koksaOsallistuiInput).toBeVisible(); + const koksaPisteetInput = koksakoe.getByLabel('Pisteet'); + await expect(koksaPisteetInput).toBeVisible(); + const koksaOsallistuiInput = koksakoe.getByText('Osallistui', { + exact: true, + }); + await expect(koksaOsallistuiInput).toBeVisible(); - const koksaTallennaButton = koksakoe.getByRole('button', { - name: 'Tallenna', + const koksaTallennaButton = koksakoe.getByRole('button', { + name: 'Tallenna', + }); + await expect(koksaTallennaButton).toBeEnabled(); }); - await expect(koksaTallennaButton).toBeEnabled(); -}); -test('Sends right data when saving pistesyotto and shows success toast', async ({ - page, -}) => { - const pisteetSaveUrl = configuration.koostetutPistetiedotHakukohteelleUrl({ - hakuOid: '1.2.246.562.29.00000000000000045102', - hakukohdeOid: HAKUKOHDE_OID, - }); + test('Sends right data when saving pistesyotto and shows success toast', async ({ + page, + }) => { + const pisteetSaveUrl = configuration.koostetutPistetiedotHakukohteelleUrl({ + hakuOid: '1.2.246.562.29.00000000000000045102', + hakukohdeOid: HAKUKOHDE_OID, + }); - await page.route(pisteetSaveUrl, (route) => { - if (route.request().method() === 'PUT') { - return route.fulfill({ - status: 200, - }); - } - }); + await page.route(pisteetSaveUrl, (route) => { + if (route.request().method() === 'PUT') { + return route.fulfill({ + status: 200, + }); + } + }); - await page.goto( - '/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000045102/henkilo/1.2.246.562.11.00000000000001796027', - ); + await page.goto( + '/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000045102/henkilo/1.2.246.562.11.00000000000001796027', + ); - await expectAllSpinnersHidden(page); + await expectAllSpinnersHidden(page); - const pistesyottoHeading = page.getByRole('heading', { name: 'Pistesyöttö' }); - await expect(pistesyottoHeading).toBeVisible(); + const pistesyottoHeading = page.getByRole('heading', { + name: 'Pistesyöttö', + }); + await expect(pistesyottoHeading).toBeVisible(); - const nakkikoe = page.getByRole('region', { - name: 'Nakkikoe, oletko nakkisuojassa?', - }); + const nakkikoe = page.getByRole('region', { + name: 'Nakkikoe, oletko nakkisuojassa?', + }); - await selectOption(page, 'Osallistumisen tila', 'Ei osallistunut', nakkikoe); + await selectOption( + page, + 'Osallistumisen tila', + 'Ei osallistunut', + nakkikoe, + ); - const nakkiTallennaButton = nakkikoe.getByRole('button', { - name: 'Tallenna', + const nakkiTallennaButton = nakkikoe.getByRole('button', { + name: 'Tallenna', + }); + + const [saveRes] = await Promise.all([ + page.waitForRequest( + (request) => + request.url() === pisteetSaveUrl && request.method() === 'PUT', + ), + nakkiTallennaButton.click(), + ]); + + expect(saveRes.postDataJSON()).toMatchObject([ + { + oid: '1.2.246.562.11.00000000000001796027', + personOid: '1.2.246.562.24.69259807406', + firstNames: 'Ruhtinas', + lastName: 'Nukettaja', + additionalData: { + 'nakki-osallistuminen': ValintakoeOsallistuminenTulos.EI_OSALLISTUNUT, + }, + }, + ]); + + await expect(page.getByText('Tiedot tallennettu')).toBeVisible(); }); - const [saveRes] = await Promise.all([ - page.waitForRequest( - (request) => - request.url() === pisteetSaveUrl && request.method() === 'PUT', - ), - nakkiTallennaButton.click(), - ]); + test('Saving pistesyöttö shows error toast', async ({ page }) => { + const pisteetSaveUrl = configuration.koostetutPistetiedotHakukohteelleUrl({ + hakuOid: '1.2.246.562.29.00000000000000045102', + hakukohdeOid: HAKUKOHDE_OID, + }); - expect(saveRes.postDataJSON()).toMatchObject([ - { - oid: '1.2.246.562.11.00000000000001796027', - personOid: '1.2.246.562.24.69259807406', - firstNames: 'Ruhtinas', - lastName: 'Nukettaja', - additionalData: { - 'nakki-osallistuminen': ValintakoeOsallistuminenTulos.EI_OSALLISTUNUT, - }, - }, - ]); + await page.route(pisteetSaveUrl, (route) => { + if (route.request().method() === 'PUT') { + return route.fulfill({ + status: 400, + }); + } + }); - await expect(page.getByText('Tiedot tallennettu')).toBeVisible(); -}); + await page.goto( + '/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000045102/henkilo/1.2.246.562.11.00000000000001796027', + ); -test('Saving pistesyöttö shows error toast', async ({ page }) => { - const pisteetSaveUrl = configuration.koostetutPistetiedotHakukohteelleUrl({ - hakuOid: '1.2.246.562.29.00000000000000045102', - hakukohdeOid: HAKUKOHDE_OID, - }); + const nakkikoe = page.getByRole('region', { + name: 'Nakkikoe, oletko nakkisuojassa?', + }); - await page.route(pisteetSaveUrl, (route) => { - if (route.request().method() === 'PUT') { - return route.fulfill({ - status: 400, - }); - } + await selectOption( + page, + 'Osallistumisen tila', + 'Ei osallistunut', + nakkikoe, + ); + + const nakkiTallennaButton = nakkikoe.getByRole('button', { + name: 'Tallenna', + }); + + await Promise.all([ + page.waitForRequest( + (request) => + request.url() === pisteetSaveUrl && request.method() === 'PUT', + ), + nakkiTallennaButton.click(), + ]); + + await expect( + page.getByText('Tietojen tallentamisessa tapahtui virhe.'), + ).toBeVisible(); }); +}); +const startLaskenta = async (page: Page) => { await page.goto( '/valintojen-toteuttaminen/haku/1.2.246.562.29.00000000000000045102/henkilo/1.2.246.562.11.00000000000001796027', ); - const nakkikoe = page.getByRole('region', { - name: 'Nakkikoe, oletko nakkisuojassa?', + const valintalaskentaButton = page.getByRole('button', { + name: 'Suorita valintalaskenta', }); - await selectOption(page, 'Osallistumisen tila', 'Ei osallistunut', nakkikoe); + await expect(valintalaskentaButton).toBeVisible(); + await valintalaskentaButton.click(); - const nakkiTallennaButton = nakkikoe.getByRole('button', { - name: 'Tallenna', + await page.getByRole('button', { name: 'Kyllä' }).click(); +}; + +test.describe('Valintalaskenta', () => { + test('shows error when starting valintalaskenta fails', async ({ page }) => { + await page.route( + '*/**/resources/valintalaskentakerralla/haku/1.2.246.562.29.00000000000000045102/tyyppi/HAKUKOHDE/whitelist/true**', + async (route) => { + await route.fulfill({ status: 500, body: 'Unknown error' }); + }, + ); + await startLaskenta(page); + + const error = page.getByText('Valintalaskenta epäonnistuiUnknown error'); + await expect(error).toBeVisible(); + }); + + test('succesfully starts laskenta and shows yhteenveto errors', async ({ + page, + }) => { + await page.route( + '*/**/resources/valintalaskentakerralla/haku/1.2.246.562.29.00000000000000045102/tyyppi/HAKUKOHDE/whitelist/true**', + async (route) => { + const started = { + lisatiedot: { + luotiinkoUusiLaskenta: true, + }, + latausUrl: '12345abs', + }; + await route.fulfill({ + json: started, + }); + }, + ); + await page.route( + '*/**/valintalaskenta-laskenta-service/resources/seuranta/yhteenveto/12345abs', + async (route) => { + const seuranta = { + tila: 'VALMIS', + hakukohteitaYhteensa: 1, + hakukohteitaValmiina: 1, + hakukohteitaKeskeytetty: 0, + }; + await route.fulfill({ + json: seuranta, + }); + }, + ); + await page.route( + '*/**/resources/valintalaskentakerralla/status/12345abs/yhteenveto', + async (route) => { + await route.fulfill({ + json: { + hakukohteet: [ + { + hakukohdeOid: '1.2.246.562.20.00000000000000045105', + tila: 'VIRHE', + ilmoitukset: [ + { + otsikko: 'Unknown Error', + }, + ], + }, + ], + }, + }); + }, + ); + await startLaskenta(page); + await expect( + page.getByText( + 'Laskenta on päättynyt. Hakukohteita valmiina 0/1. Suorittamattomia hakukohteita 1.', + ), + ).toBeVisible(); + + await page + .getByRole('button', { name: 'Näytä suorittamattomat hakukohteet' }) + .click(); + + await expect( + page.getByText( + `Tampereen yliopisto, Rakennetun ympäristön tiedekunta ${NDASH} Finnish MAOL competition route, Technology, Sustainable Urban Development, Bachelor and Master of Science (Technology) (3 + 2 yrs) (1.2.246.562.20.00000000000000045105)`, + ), + ).toBeVisible(); + await expect(page.getByText('Unknown Error')).toBeVisible(); + + await expect( + page.getByRole('button', { name: 'Suorita valintalaskenta' }), + ).toBeHidden(); + + await page.getByRole('button', { name: 'Sulje laskennan tiedot' }).click(); + + await expect( + page.getByRole('button', { name: 'Suorita valintalaskenta' }), + ).toBeVisible(); }); - await Promise.all([ - page.waitForRequest( - (request) => - request.url() === pisteetSaveUrl && request.method() === 'PUT', - ), - nakkiTallennaButton.click(), - ]); + test('shows results with success toast', async ({ page }) => { + await page.route( + '*/**/resources/valintalaskentakerralla/haku/1.2.246.562.29.00000000000000045102/tyyppi/HAKUKOHDE/whitelist/true**', + async (route) => { + const started = { + lisatiedot: { + luotiinkoUusiLaskenta: true, + }, + latausUrl: '12345abs', + }; + await route.fulfill({ + json: started, + }); + }, + ); + await page.route( + '*/**/valintalaskenta-laskenta-service/resources/seuranta/yhteenveto/12345abs', + async (route) => { + await route.fulfill({ + json: { + tila: 'VALMIS', + hakukohteitaYhteensa: 1, + hakukohteitaValmiina: 1, + hakukohteitaKeskeytetty: 0, + }, + }); + }, + ); + await page.route( + '*/**/resources/valintalaskentakerralla/status/12345abs/yhteenveto', + async (route) => { + await route.fulfill({ + json: { + tila: 'VALMIS', + hakukohteet: [ + { + hakukohdeOid: '1.2.246.562.20.00000000000000045105', + tila: 'VALMIS', + ilmoitukset: [], + }, + ], + }, + }); + }, + ); + await startLaskenta(page); + await expect( + page.getByText( + 'Laskenta on päättynyt. Hakukohteita valmiina 1/1. Suorittamattomia hakukohteita 0.', + ), + ).toBeVisible(); + await expect( + page.getByText('Laskenta suoritettu onnistuneesti'), + ).toBeVisible(); - await expect( - page.getByText('Tietojen tallentamisessa tapahtui virhe.'), - ).toBeVisible(); + await expect( + page.getByRole('button', { name: 'Näytä suorittamattomat hakukohteet' }), + ).toBeHidden(); + + await expect( + page.getByRole('button', { name: 'Suorita valintalaskenta' }), + ).toBeHidden(); + + await page.getByRole('button', { name: 'Sulje laskennan tiedot' }).click(); + + await expect( + page.getByRole('button', { name: 'Suorita valintalaskenta' }), + ).toBeVisible(); + }); }); diff --git a/tests/e2e/valinnan-hallinta.spec.ts b/tests/e2e/valinnan-hallinta.spec.ts index 355a8a3d..ccdae0ca 100644 --- a/tests/e2e/valinnan-hallinta.spec.ts +++ b/tests/e2e/valinnan-hallinta.spec.ts @@ -102,8 +102,10 @@ test('shows success toast when laskenta completes', async ({ page }) => { async (route) => { await route.fulfill({ json: { + tila: 'VALMIS', hakukohteet: [ { + tila: 'VALMIS', hakukohde: '1.2.246.562.20.00000000000000045105', ilmoitukset: [], },