From c2a74b716f7b3b732af5ab8751ee43f14fe481a2 Mon Sep 17 00:00:00 2001 From: Michael Markl Date: Mon, 14 Nov 2022 12:03:54 +0100 Subject: [PATCH] Add application frontend (#594) * Start with the application form * Fix application endpoints * Advance application frontend * Improve DateForm * Add newline * Improve responsivity * Improve state handling * Refactor application to use streamlined form concept * Fix value property * Fix submit callback in ApplyController * Fix FileInputForm * Don't JSON.stringify localforage * Use empty object for unused options instead of void * Garbage-collect ArrayBuffers * Fix AddressForm * Change naming from setOpen to onUpdateOpen * Make forms upper case * Remove @mui/lab dependency * Replace alert() with a Snackbar * Remove duplicate SnackbarProvider --- .idea/modules.xml | 1 + administration/.prettierignore | 1 + administration/administration.iml | 1 + administration/package-lock.json | 890 +++++++++++++++++- administration/package.json | 14 +- administration/src/App.tsx | 9 +- administration/src/application/FormType.ts | 27 + .../components/ApplyController.tsx | 80 ++ .../application/components/ConfirmDialog.tsx | 38 + .../components/DiscardAllInputsButton.tsx | 24 + .../application/components/SwitchDisplay.tsx | 15 + .../components/forms/AddressForm.tsx | 89 ++ .../components/forms/ApplicationForm.tsx | 98 ++ .../components/forms/OrganizationForm.tsx | 118 +++ .../components/forms/PersonalDataForm.tsx | 107 +++ .../forms/StandardEntitlementForm.tsx | 94 ++ .../forms/WorkAtOrganizationForm.tsx | 149 +++ .../components/primitive-inputs/DateForm.tsx | 45 + .../components/primitive-inputs/EmailForm.tsx | 40 + .../primitive-inputs/FileInputForm.tsx | 101 ++ .../primitive-inputs/NumberForm.tsx | 43 + .../primitive-inputs/SelectForm.tsx | 46 + .../primitive-inputs/ShortTextForm.tsx | 48 + .../application/globalArrayBuffersManager.ts | 70 ++ .../src/application/useLocallyStoredState.ts | 47 + .../application/useUpdateStateCallback.tsx | 9 + .../src/graphql/applications/apply.graphql | 3 + .../EakApplicationMutationService.kt | 23 +- .../webservice/EakApplicationQueryService.kt | 13 +- 29 files changed, 2202 insertions(+), 41 deletions(-) create mode 100644 administration/src/application/FormType.ts create mode 100644 administration/src/application/components/ApplyController.tsx create mode 100644 administration/src/application/components/ConfirmDialog.tsx create mode 100644 administration/src/application/components/DiscardAllInputsButton.tsx create mode 100644 administration/src/application/components/SwitchDisplay.tsx create mode 100644 administration/src/application/components/forms/AddressForm.tsx create mode 100644 administration/src/application/components/forms/ApplicationForm.tsx create mode 100644 administration/src/application/components/forms/OrganizationForm.tsx create mode 100644 administration/src/application/components/forms/PersonalDataForm.tsx create mode 100644 administration/src/application/components/forms/StandardEntitlementForm.tsx create mode 100644 administration/src/application/components/forms/WorkAtOrganizationForm.tsx create mode 100644 administration/src/application/components/primitive-inputs/DateForm.tsx create mode 100644 administration/src/application/components/primitive-inputs/EmailForm.tsx create mode 100644 administration/src/application/components/primitive-inputs/FileInputForm.tsx create mode 100644 administration/src/application/components/primitive-inputs/NumberForm.tsx create mode 100644 administration/src/application/components/primitive-inputs/SelectForm.tsx create mode 100644 administration/src/application/components/primitive-inputs/ShortTextForm.tsx create mode 100644 administration/src/application/globalArrayBuffersManager.ts create mode 100644 administration/src/application/useLocallyStoredState.ts create mode 100644 administration/src/application/useUpdateStateCallback.tsx create mode 100644 administration/src/graphql/applications/apply.graphql diff --git a/.idea/modules.xml b/.idea/modules.xml index 78fc33bfa..a54760d0a 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -5,6 +5,7 @@ + \ No newline at end of file diff --git a/administration/.prettierignore b/administration/.prettierignore index c83f90a2b..22cb21cad 100644 --- a/administration/.prettierignore +++ b/administration/.prettierignore @@ -1 +1,2 @@ src/generated +build diff --git a/administration/administration.iml b/administration/administration.iml index 2e839296b..9c05f0914 100644 --- a/administration/administration.iml +++ b/administration/administration.iml @@ -5,6 +5,7 @@ + diff --git a/administration/package-lock.json b/administration/package-lock.json index 3c25ae0c1..7a2b36027 100644 --- a/administration/package-lock.json +++ b/administration/package-lock.json @@ -14,14 +14,22 @@ "@blueprintjs/datetime": "^4.3.12", "@blueprintjs/popover2": "^1.7.3", "@blueprintjs/select": "^4.6.6", + "@emotion/react": "^11.10.4", + "@emotion/styled": "^11.10.4", + "@fontsource/roboto": "^4.5.8", + "@mui/icons-material": "^5.10.9", + "@mui/material": "^5.10.9", "@zxing/library": "^0.19.1", + "apollo-upload-client": "^17.0.0", "core-js": "^3.25.2", "date-fns": "^2.29.3", "detect-browser": "^5.3.0", "fast-text-encoding": "^1.0.6", "graphql": "^16.6.0", "jspdf": "^2.5.1", + "localforage": "^1.10.0", "long": "^5.2.0", + "notistack": "^2.0.8", "protobufjs": "^7.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -37,6 +45,7 @@ "@graphql-codegen/typescript": "2.7.3", "@graphql-codegen/typescript-operations": "2.5.3", "@graphql-codegen/typescript-react-apollo": "3.3.3", + "@types/apollo-upload-client": "^17.0.2", "@types/blob-stream": "^0.1.30", "@types/jest": "^29.0.3", "@types/node": "^18.7.18", @@ -64,18 +73,19 @@ } }, "node_modules/@apollo/client": { - "version": "3.6.9", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.6.9.tgz", - "integrity": "sha512-Y1yu8qa2YeaCUBVuw08x8NHenFi0sw2I3KCu7Kw9mDSu86HmmtHJkCAifKVrN2iPgDTW/BbP3EpSV8/EQCcxZA==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.7.0.tgz", + "integrity": "sha512-hp4OvrH1ZIQACRYcIrh/C0WFnY7IM7G6nlTpC8DSTEWxfZQ2kvpvDY0I/hYmCs0oAVrg26g3ANEdOzGWTcYbPg==", "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", - "@wry/context": "^0.6.0", + "@wry/context": "^0.7.0", "@wry/equality": "^0.5.0", "@wry/trie": "^0.3.0", "graphql-tag": "^2.12.6", "hoist-non-react-statics": "^3.3.2", "optimism": "^0.16.1", "prop-types": "^15.7.2", + "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", "tslib": "^2.3.0", @@ -85,6 +95,7 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", "graphql-ws": "^5.5.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" }, "peerDependenciesMeta": { @@ -94,11 +105,25 @@ "react": { "optional": true }, + "react-dom": { + "optional": true + }, "subscriptions-transport-ws": { "optional": true } } }, + "node_modules/@apollo/client/node_modules/@wry/context": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.0.tgz", + "integrity": "sha512-LcDAiYWRtwAoSOArfk7cuYvFXytxfVrdX7yxoUmK7pPITLk5jYh2F8knCwS7LjgYL8u1eidPlKKV6Ikqq0ODqQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@apollo/utils.keyvaluecache": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-1.0.1.tgz", @@ -3004,6 +3029,53 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.10.2", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.2.tgz", + "integrity": "sha512-xNQ57njWTFVfPAc3cjfuaPdsgLp5QOSuRsj9MA6ndEhH/AzuZM86qIQzt6rq+aGBwj3n5/TkLmU5lhAfdRmogA==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/plugin-syntax-jsx": "^7.17.12", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/serialize": "^1.1.0", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.0.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.10.3", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.3.tgz", + "integrity": "sha512-Psmp/7ovAa8appWh3g51goxu/z3iVms7JXOreq136D8Bbn6dYraPnmL6mdM8GThEx9vwSn92Fz+mGSjBzN8UPQ==", + "dependencies": { + "@emotion/memoize": "^0.8.0", + "@emotion/sheet": "^1.2.0", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", + "stylis": "4.0.13" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", @@ -3017,6 +3089,81 @@ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" }, + "node_modules/@emotion/react": { + "version": "11.10.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.4.tgz", + "integrity": "sha512-j0AkMpr6BL8gldJZ6XQsQ8DnS9TxEQu1R+OGmDZiWjBAJtCcbt0tS3I/YffoqHXxH6MjgI7KdMbYKw3MEiU9eA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.0", + "@emotion/cache": "^11.10.0", + "@emotion/serialize": "^1.1.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.0.tgz", + "integrity": "sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/unitless": "^0.8.0", + "@emotion/utils": "^1.2.0", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/unitless": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + }, + "node_modules/@emotion/sheet": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.0.tgz", + "integrity": "sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==" + }, + "node_modules/@emotion/styled": { + "version": "11.10.4", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.4.tgz", + "integrity": "sha512-pRl4R8Ez3UXvOPfc2bzIoV8u9P97UedgHS4FPX594ntwEuAMA114wlaHvOK24HB48uqfXiGlYIZYCxVJ1R1ttQ==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.0", + "@emotion/is-prop-valid": "^1.2.0", + "@emotion/serialize": "^1.1.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", + "@emotion/utils": "^1.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/@emotion/stylis": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", @@ -3027,6 +3174,24 @@ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", + "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", + "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + }, "node_modules/@endemolshinegroup/cosmiconfig-typescript-loader": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz", @@ -3128,6 +3293,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@fontsource/roboto": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz", + "integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA==" + }, "node_modules/@graphql-codegen/cli": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.12.0.tgz", @@ -4776,6 +4946,247 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, + "node_modules/@mui/base": { + "version": "5.0.0-alpha.101", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.101.tgz", + "integrity": "sha512-a54BcXvArGOKUZ2zyS/7B9GNhAGgfomEQSkfEZ88Nc9jKvXA+Mppenfz5o4JCAnD8c4VlePmz9rKOYvvum1bZw==", + "dependencies": { + "@babel/runtime": "^7.19.0", + "@emotion/is-prop-valid": "^1.2.0", + "@mui/types": "^7.2.0", + "@mui/utils": "^5.10.9", + "@popperjs/core": "^2.11.6", + "clsx": "^1.2.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.9.tgz", + "integrity": "sha512-rqoFu4qww6KJBbXYhyRd9YXjwBHa3ylnBPSWbGf1bdfG0AYMKmVzg8zxkWvxAWOp97kvx3M2kNPb0xMIDZiogQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.10.9.tgz", + "integrity": "sha512-sqClXdEM39WKQJOQ0ZCPTptaZgqwibhj2EFV9N0v7BU1PO8y4OcX/a2wIQHn4fNuDjIZktJIBrmU23h7aqlGgg==", + "dependencies": { + "@babel/runtime": "^7.19.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.10.9.tgz", + "integrity": "sha512-sdOzlgpCmyw48je+E7o9UGGJpgBaF+60FlTRpVpcd/z+LUhnuzzuis891yPI5dPPXLBDL/bO4SsGg51lgNeLBw==", + "dependencies": { + "@babel/runtime": "^7.19.0", + "@mui/base": "5.0.0-alpha.101", + "@mui/core-downloads-tracker": "^5.10.9", + "@mui/system": "^5.10.9", + "@mui/types": "^7.2.0", + "@mui/utils": "^5.10.9", + "@types/react-transition-group": "^4.4.5", + "clsx": "^1.2.1", + "csstype": "^3.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.10.9.tgz", + "integrity": "sha512-BN7/CnsVPVyBaQpDTij4uV2xGYHHHhOgpdxeYLlIu+TqnsVM7wUeF+37kXvHovxM6xmL5qoaVUD98gDC0IZnHg==", + "dependencies": { + "@babel/runtime": "^7.19.0", + "@mui/utils": "^5.10.9", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.10.8", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.10.8.tgz", + "integrity": "sha512-w+y8WI18EJV6zM/q41ug19cE70JTeO6sWFsQ7tgePQFpy6ToCVPh0YLrtqxUZXSoMStW5FMw0t9fHTFAqPbngw==", + "dependencies": { + "@babel/runtime": "^7.19.0", + "@emotion/cache": "^11.10.3", + "csstype": "^3.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.10.10", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.10.10.tgz", + "integrity": "sha512-TXwtKN0adKpBrZmO+eilQWoPf2veh050HLYrN78Kps9OhlvO70v/2Kya0+mORFhu9yhpAwjHXO8JII/R4a5ZLA==", + "dependencies": { + "@babel/runtime": "^7.19.0", + "@mui/private-theming": "^5.10.9", + "@mui/styled-engine": "^5.10.8", + "@mui/types": "^7.2.0", + "@mui/utils": "^5.10.9", + "clsx": "^1.2.1", + "csstype": "^3.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.0.tgz", + "integrity": "sha512-lGXtFKe5lp3UxTBGqKI1l7G8sE2xBik8qCfrLHD5olwP/YU0/ReWoWT7Lp1//ri32dK39oPMrJN8TgbkCSbsNA==", + "peerDependencies": { + "@types/react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.10.9.tgz", + "integrity": "sha512-2tdHWrq3+WCy+G6TIIaFx3cg7PorXZ71P375ExuX61od1NOAJP1mK90VxQ8N4aqnj2vmO3AQDkV4oV2Ktvt4bA==", + "dependencies": { + "@babel/runtime": "^7.19.0", + "@types/prop-types": "^15.7.5", + "@types/react-is": "^16.7.1 || ^17.0.0", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -5788,6 +6199,17 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "node_modules/@types/apollo-upload-client": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/@types/apollo-upload-client/-/apollo-upload-client-17.0.2.tgz", + "integrity": "sha512-NphAiBqzZv3iY8Cq+qWyi0QUFFzJ+nVd7QKI/iKV8RfILrpYDL69F/vlhjn4BNxKlmc3LxJHymcf3gFzLBwuZQ==", + "dev": true, + "dependencies": { + "@apollo/client": "^3.7.0", + "@types/extract-files": "*", + "graphql": "14 - 16" + } + }, "node_modules/@types/babel__core": { "version": "7.1.19", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", @@ -5917,6 +6339,12 @@ "@types/range-parser": "*" } }, + "node_modules/@types/extract-files": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/extract-files/-/extract-files-8.1.1.tgz", + "integrity": "sha512-dMJJqBqyhsfJKuK7p7HyyNmki7qj1AlwhUKWx6KrU7i1K2T2SPsUsSUTWFmr/sEM1q8rfR8j5IyUmYrDbrhfjQ==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -6097,8 +6525,7 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "devOptional": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/q": { "version": "1.5.5", @@ -6125,7 +6552,6 @@ "version": "18.0.20", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.20.tgz", "integrity": "sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA==", - "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -6141,6 +6567,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-is": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", + "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-router": { "version": "5.1.19", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.19.tgz", @@ -6162,6 +6596,14 @@ "@types/react-router": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -6178,8 +6620,7 @@ "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "devOptional": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "node_modules/@types/serve-index": { "version": "1.9.1", @@ -7561,6 +8002,24 @@ "node": ">=12.0" } }, + "node_modules/apollo-upload-client": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-17.0.0.tgz", + "integrity": "sha512-pue33bWVbdlXAGFPkgz53TTmxVMrKeQr0mdRcftNY+PoHIdbGZD0hoaXHvO6OePJAkFz7OiCFUf98p1G/9+Ykw==", + "dependencies": { + "extract-files": "^11.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >= 16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/jaydenseric" + }, + "peerDependencies": { + "@apollo/client": "^3.0.0", + "graphql": "14 - 16" + } + }, "node_modules/apollo/node_modules/apollo-graphql": { "version": "0.9.7", "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.9.7.tgz", @@ -8924,6 +9383,14 @@ "node": ">=0.8" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -11386,7 +11853,6 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz", "integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==", - "dev": true, "engines": { "node": "^12.20 || >= 14.13" }, @@ -11647,6 +12113,11 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -13015,6 +13486,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/immer": { "version": "9.0.15", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz", @@ -16131,6 +16607,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -16604,6 +17088,14 @@ "node": ">=8.9.0" } }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dependencies": { + "lie": "3.1.1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -17432,6 +17924,34 @@ "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" }, + "node_modules/notistack": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-2.0.8.tgz", + "integrity": "sha512-/IY14wkFp5qjPgKNvAdfL5Jp6q90+MjgKTPh4c81r/lW70KeuX6b9pE/4f8L4FG31cNudbN9siiFS5ql1aSLRw==", + "dependencies": { + "clsx": "^1.1.0", + "hoist-non-react-statics": "^3.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/notistack" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "@mui/material": "^5.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -20579,6 +21099,14 @@ "node": ">=10" } }, + "node_modules/response-iterator": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/response-iterator/-/response-iterator-0.2.6.tgz", + "integrity": "sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -21750,6 +22278,11 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz", + "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==" + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -24013,22 +24546,33 @@ } }, "@apollo/client": { - "version": "3.6.9", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.6.9.tgz", - "integrity": "sha512-Y1yu8qa2YeaCUBVuw08x8NHenFi0sw2I3KCu7Kw9mDSu86HmmtHJkCAifKVrN2iPgDTW/BbP3EpSV8/EQCcxZA==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.7.0.tgz", + "integrity": "sha512-hp4OvrH1ZIQACRYcIrh/C0WFnY7IM7G6nlTpC8DSTEWxfZQ2kvpvDY0I/hYmCs0oAVrg26g3ANEdOzGWTcYbPg==", "requires": { "@graphql-typed-document-node/core": "^3.1.1", - "@wry/context": "^0.6.0", + "@wry/context": "^0.7.0", "@wry/equality": "^0.5.0", "@wry/trie": "^0.3.0", "graphql-tag": "^2.12.6", "hoist-non-react-statics": "^3.3.2", "optimism": "^0.16.1", "prop-types": "^15.7.2", + "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", "tslib": "^2.3.0", "zen-observable-ts": "^1.2.5" + }, + "dependencies": { + "@wry/context": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.0.tgz", + "integrity": "sha512-LcDAiYWRtwAoSOArfk7cuYvFXytxfVrdX7yxoUmK7pPITLk5jYh2F8knCwS7LjgYL8u1eidPlKKV6Ikqq0ODqQ==", + "requires": { + "tslib": "^2.3.0" + } + } } }, "@apollo/utils.keyvaluecache": { @@ -26052,6 +26596,49 @@ "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==", "requires": {} }, + "@emotion/babel-plugin": { + "version": "11.10.2", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.2.tgz", + "integrity": "sha512-xNQ57njWTFVfPAc3cjfuaPdsgLp5QOSuRsj9MA6ndEhH/AzuZM86qIQzt6rq+aGBwj3n5/TkLmU5lhAfdRmogA==", + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/plugin-syntax-jsx": "^7.17.12", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/serialize": "^1.1.0", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.0.13" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + } + } + }, + "@emotion/cache": { + "version": "11.10.3", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.3.tgz", + "integrity": "sha512-Psmp/7ovAa8appWh3g51goxu/z3iVms7JXOreq136D8Bbn6dYraPnmL6mdM8GThEx9vwSn92Fz+mGSjBzN8UPQ==", + "requires": { + "@emotion/memoize": "^0.8.0", + "@emotion/sheet": "^1.2.0", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", + "stylis": "4.0.13" + } + }, + "@emotion/hash": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + }, "@emotion/is-prop-valid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", @@ -26065,6 +26652,58 @@ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" }, + "@emotion/react": { + "version": "11.10.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.4.tgz", + "integrity": "sha512-j0AkMpr6BL8gldJZ6XQsQ8DnS9TxEQu1R+OGmDZiWjBAJtCcbt0tS3I/YffoqHXxH6MjgI7KdMbYKw3MEiU9eA==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.0", + "@emotion/cache": "^11.10.0", + "@emotion/serialize": "^1.1.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", + "hoist-non-react-statics": "^3.3.1" + } + }, + "@emotion/serialize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.0.tgz", + "integrity": "sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==", + "requires": { + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/unitless": "^0.8.0", + "@emotion/utils": "^1.2.0", + "csstype": "^3.0.2" + }, + "dependencies": { + "@emotion/unitless": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + } + } + }, + "@emotion/sheet": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.0.tgz", + "integrity": "sha512-OiTkRgpxescko+M51tZsMq7Puu/KP55wMT8BgpcXVG2hqXc0Vo0mfymJ/Uj24Hp0i083ji/o0aLddh08UEjq8w==" + }, + "@emotion/styled": { + "version": "11.10.4", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.4.tgz", + "integrity": "sha512-pRl4R8Ez3UXvOPfc2bzIoV8u9P97UedgHS4FPX594ntwEuAMA114wlaHvOK24HB48uqfXiGlYIZYCxVJ1R1ttQ==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.0", + "@emotion/is-prop-valid": "^1.2.0", + "@emotion/serialize": "^1.1.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", + "@emotion/utils": "^1.2.0" + } + }, "@emotion/stylis": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", @@ -26075,6 +26714,22 @@ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, + "@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", + "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", + "requires": {} + }, + "@emotion/utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + }, + "@emotion/weak-memoize": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", + "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + }, "@endemolshinegroup/cosmiconfig-typescript-loader": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz", @@ -26148,6 +26803,11 @@ } } }, + "@fontsource/roboto": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz", + "integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA==" + }, "@graphql-codegen/cli": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.12.0.tgz", @@ -27478,6 +28138,107 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, + "@mui/base": { + "version": "5.0.0-alpha.101", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.101.tgz", + "integrity": "sha512-a54BcXvArGOKUZ2zyS/7B9GNhAGgfomEQSkfEZ88Nc9jKvXA+Mppenfz5o4JCAnD8c4VlePmz9rKOYvvum1bZw==", + "requires": { + "@babel/runtime": "^7.19.0", + "@emotion/is-prop-valid": "^1.2.0", + "@mui/types": "^7.2.0", + "@mui/utils": "^5.10.9", + "@popperjs/core": "^2.11.6", + "clsx": "^1.2.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + } + }, + "@mui/core-downloads-tracker": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.9.tgz", + "integrity": "sha512-rqoFu4qww6KJBbXYhyRd9YXjwBHa3ylnBPSWbGf1bdfG0AYMKmVzg8zxkWvxAWOp97kvx3M2kNPb0xMIDZiogQ==" + }, + "@mui/icons-material": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.10.9.tgz", + "integrity": "sha512-sqClXdEM39WKQJOQ0ZCPTptaZgqwibhj2EFV9N0v7BU1PO8y4OcX/a2wIQHn4fNuDjIZktJIBrmU23h7aqlGgg==", + "requires": { + "@babel/runtime": "^7.19.0" + } + }, + "@mui/material": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.10.9.tgz", + "integrity": "sha512-sdOzlgpCmyw48je+E7o9UGGJpgBaF+60FlTRpVpcd/z+LUhnuzzuis891yPI5dPPXLBDL/bO4SsGg51lgNeLBw==", + "requires": { + "@babel/runtime": "^7.19.0", + "@mui/base": "5.0.0-alpha.101", + "@mui/core-downloads-tracker": "^5.10.9", + "@mui/system": "^5.10.9", + "@mui/types": "^7.2.0", + "@mui/utils": "^5.10.9", + "@types/react-transition-group": "^4.4.5", + "clsx": "^1.2.1", + "csstype": "^3.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + } + }, + "@mui/private-theming": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.10.9.tgz", + "integrity": "sha512-BN7/CnsVPVyBaQpDTij4uV2xGYHHHhOgpdxeYLlIu+TqnsVM7wUeF+37kXvHovxM6xmL5qoaVUD98gDC0IZnHg==", + "requires": { + "@babel/runtime": "^7.19.0", + "@mui/utils": "^5.10.9", + "prop-types": "^15.8.1" + } + }, + "@mui/styled-engine": { + "version": "5.10.8", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.10.8.tgz", + "integrity": "sha512-w+y8WI18EJV6zM/q41ug19cE70JTeO6sWFsQ7tgePQFpy6ToCVPh0YLrtqxUZXSoMStW5FMw0t9fHTFAqPbngw==", + "requires": { + "@babel/runtime": "^7.19.0", + "@emotion/cache": "^11.10.3", + "csstype": "^3.1.1", + "prop-types": "^15.8.1" + } + }, + "@mui/system": { + "version": "5.10.10", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.10.10.tgz", + "integrity": "sha512-TXwtKN0adKpBrZmO+eilQWoPf2veh050HLYrN78Kps9OhlvO70v/2Kya0+mORFhu9yhpAwjHXO8JII/R4a5ZLA==", + "requires": { + "@babel/runtime": "^7.19.0", + "@mui/private-theming": "^5.10.9", + "@mui/styled-engine": "^5.10.8", + "@mui/types": "^7.2.0", + "@mui/utils": "^5.10.9", + "clsx": "^1.2.1", + "csstype": "^3.1.1", + "prop-types": "^15.8.1" + } + }, + "@mui/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.0.tgz", + "integrity": "sha512-lGXtFKe5lp3UxTBGqKI1l7G8sE2xBik8qCfrLHD5olwP/YU0/ReWoWT7Lp1//ri32dK39oPMrJN8TgbkCSbsNA==", + "requires": {} + }, + "@mui/utils": { + "version": "5.10.9", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.10.9.tgz", + "integrity": "sha512-2tdHWrq3+WCy+G6TIIaFx3cg7PorXZ71P375ExuX61od1NOAJP1mK90VxQ8N4aqnj2vmO3AQDkV4oV2Ktvt4bA==", + "requires": { + "@babel/runtime": "^7.19.0", + "@types/prop-types": "^15.7.5", + "@types/react-is": "^16.7.1 || ^17.0.0", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + } + }, "@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -28222,6 +28983,17 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "@types/apollo-upload-client": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/@types/apollo-upload-client/-/apollo-upload-client-17.0.2.tgz", + "integrity": "sha512-NphAiBqzZv3iY8Cq+qWyi0QUFFzJ+nVd7QKI/iKV8RfILrpYDL69F/vlhjn4BNxKlmc3LxJHymcf3gFzLBwuZQ==", + "dev": true, + "requires": { + "@apollo/client": "^3.7.0", + "@types/extract-files": "*", + "graphql": "14 - 16" + } + }, "@types/babel__core": { "version": "7.1.19", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", @@ -28351,6 +29123,12 @@ "@types/range-parser": "*" } }, + "@types/extract-files": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/extract-files/-/extract-files-8.1.1.tgz", + "integrity": "sha512-dMJJqBqyhsfJKuK7p7HyyNmki7qj1AlwhUKWx6KrU7i1K2T2SPsUsSUTWFmr/sEM1q8rfR8j5IyUmYrDbrhfjQ==", + "dev": true + }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -28524,8 +29302,7 @@ "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "devOptional": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "@types/q": { "version": "1.5.5", @@ -28552,7 +29329,6 @@ "version": "18.0.20", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.20.tgz", "integrity": "sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA==", - "devOptional": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -28568,6 +29344,14 @@ "@types/react": "*" } }, + "@types/react-is": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", + "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==", + "requires": { + "@types/react": "*" + } + }, "@types/react-router": { "version": "5.1.19", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.19.tgz", @@ -28589,6 +29373,14 @@ "@types/react-router": "*" } }, + "@types/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "requires": { + "@types/react": "*" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -28605,8 +29397,7 @@ "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "devOptional": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "@types/serve-index": { "version": "1.9.1", @@ -29709,6 +30500,14 @@ "node-fetch": "^2.6.7" } }, + "apollo-upload-client": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-17.0.0.tgz", + "integrity": "sha512-pue33bWVbdlXAGFPkgz53TTmxVMrKeQr0mdRcftNY+PoHIdbGZD0hoaXHvO6OePJAkFz7OiCFUf98p1G/9+Ykw==", + "requires": { + "extract-files": "^11.0.0" + } + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -30714,6 +31513,11 @@ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "dev": true }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -32539,8 +33343,7 @@ "extract-files": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz", - "integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==", - "dev": true + "integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==" }, "extract-stack": { "version": "2.0.0", @@ -32752,6 +33555,11 @@ "pkg-dir": "^4.1.0" } }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -33735,6 +34543,11 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "immer": { "version": "9.0.15", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz", @@ -36181,6 +36994,14 @@ "type-check": "~0.4.0" } }, + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "requires": { + "immediate": "~3.0.5" + } + }, "lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -36544,6 +37365,14 @@ "json5": "^2.1.2" } }, + "localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "requires": { + "lie": "3.1.1" + } + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -37168,6 +37997,15 @@ "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" }, + "notistack": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-2.0.8.tgz", + "integrity": "sha512-/IY14wkFp5qjPgKNvAdfL5Jp6q90+MjgKTPh4c81r/lW70KeuX6b9pE/4f8L4FG31cNudbN9siiFS5ql1aSLRw==", + "requires": { + "clsx": "^1.1.0", + "hoist-non-react-statics": "^3.3.0" + } + }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -39320,6 +40158,11 @@ "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==" }, + "response-iterator": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/response-iterator/-/response-iterator-0.2.6.tgz", + "integrity": "sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==" + }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -40211,6 +41054,11 @@ "postcss-selector-parser": "^6.0.4" } }, + "stylis": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz", + "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==" + }, "supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", diff --git a/administration/package.json b/administration/package.json index 1ec19ad58..6e1365323 100644 --- a/administration/package.json +++ b/administration/package.json @@ -8,14 +8,22 @@ "@blueprintjs/datetime": "^4.3.12", "@blueprintjs/popover2": "^1.7.3", "@blueprintjs/select": "^4.6.6", + "@emotion/react": "^11.10.4", + "@emotion/styled": "^11.10.4", + "@fontsource/roboto": "^4.5.8", + "@mui/icons-material": "^5.10.9", + "@mui/material": "^5.10.9", "@zxing/library": "^0.19.1", + "apollo-upload-client": "^17.0.0", "core-js": "^3.25.2", "date-fns": "^2.29.3", "detect-browser": "^5.3.0", "fast-text-encoding": "^1.0.6", "graphql": "^16.6.0", "jspdf": "^2.5.1", + "localforage": "^1.10.0", "long": "^5.2.0", + "notistack": "^2.0.8", "protobufjs": "^7.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -31,6 +39,7 @@ "@graphql-codegen/typescript": "2.7.3", "@graphql-codegen/typescript-operations": "2.5.3", "@graphql-codegen/typescript-react-apollo": "3.3.3", + "@types/apollo-upload-client": "^17.0.2", "@types/blob-stream": "^0.1.30", "@types/jest": "^29.0.3", "@types/node": "^18.7.18", @@ -61,7 +70,10 @@ "react-app", "react-app/jest" ], - "ignorePatterns": "src/generated/**" + "ignorePatterns": [ + "src/generated/**", + "build/**" + ] }, "browserslist": { "production": [ diff --git a/administration/src/App.tsx b/administration/src/App.tsx index bc5a84a80..012c0d1e3 100644 --- a/administration/src/App.tsx +++ b/administration/src/App.tsx @@ -1,6 +1,6 @@ import React from 'react' import Navigation from './components/Navigation' -import { ApolloClient, ApolloProvider, createHttpLink, InMemoryCache } from '@apollo/client' +import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client' import { setContext } from '@apollo/client/link/context' import { BrowserRouter, Route, Routes } from 'react-router-dom' import GenerationController from './components/generation/GenerationController' @@ -17,14 +17,14 @@ import { AppToasterProvider } from './components/AppToaster' import UserSettingsController from './components/user-settings/UserSettingsController' import ResetPasswordController from './components/auth/ResetPasswordController' import ForgotPasswordController from './components/auth/ForgotPasswordController' +import ApplyController from './application/components/ApplyController' +import { createUploadLink } from 'apollo-upload-client' if (!process.env.REACT_APP_API_BASE_URL) { throw new Error('REACT_APP_API_BASE_URL is not set!') } -const httpLink = createHttpLink({ - uri: process.env.REACT_APP_API_BASE_URL, -}) +const httpLink = createUploadLink({ uri: process.env.REACT_APP_API_BASE_URL }) const createAuthLink = (token?: string) => setContext((_, { headers }) => ({ @@ -58,6 +58,7 @@ const App = () => ( } /> + } /> } /> = { + initialState: State + getValidatedInput: GetValidatedInput + getArrayBufferKeys: (state: State) => number[] + Component: (props: Props) => ReactElement | null +} + +export type ValidationSuccess = { type: 'valid'; value: I } +export type ValidationError = { type: 'error'; message?: string } +export type ValidationResult = ValidationError | ValidationSuccess + +// Do not require an `options` parameter, if Options is an empty object. +export type GetValidatedInput = {} extends Options + ? (state: State, options?: Options) => ValidationResult + : (state: State, options: Options) => ValidationResult + +// Do not require `options` prop, if Options is an empty object. +type OptionsProps = {} extends Options ? { options?: Options } : { options: Options } + +type Props = AdditionalProps & + OptionsProps & { + state: State + setState: SetState + } diff --git a/administration/src/application/components/ApplyController.tsx b/administration/src/application/components/ApplyController.tsx new file mode 100644 index 000000000..3c06b96cc --- /dev/null +++ b/administration/src/application/components/ApplyController.tsx @@ -0,0 +1,80 @@ +import '@fontsource/roboto/300.css' +import '@fontsource/roboto/400.css' +import '@fontsource/roboto/500.css' +import '@fontsource/roboto/700.css' +import SendIcon from '@mui/icons-material/Send' + +import { useAddBlueEakApplicationMutation } from '../../generated/graphql' +import { Button, DialogActions } from '@mui/material' +import useLocallyStoredState from '../useLocallyStoredState' +import DiscardAllInputsButton from './DiscardAllInputsButton' +import { useGarbageCollectArrayBuffers, useInitializeGlobalArrayBuffersManager } from '../globalArrayBuffersManager' +import ApplicationForm from './forms/ApplicationForm' +import { useMemo } from 'react' +import { SnackbarProvider, useSnackbar } from 'notistack' + +const applicationStorageKey = 'applicationState' + +const ApplyController = () => { + const [addBlueEakApplication] = useAddBlueEakApplicationMutation() + const [state, setState] = useLocallyStoredState(ApplicationForm.initialState, applicationStorageKey) + const arrayBufferManagerInitialized = useInitializeGlobalArrayBuffersManager() + const getArrayBufferKeys = useMemo( + () => (state === null ? null : () => ApplicationForm.getArrayBufferKeys(state)), + [state] + ) + const { enqueueSnackbar } = useSnackbar() + useGarbageCollectArrayBuffers(getArrayBufferKeys) + // state is null, if it's still being loaded from storage (e.g. after a page reload) + if (state == null || !arrayBufferManagerInitialized) { + return null + } + + const submit = () => { + const application = ApplicationForm.getValidatedInput(state) + if (application.type === 'error') { + enqueueSnackbar('Ungültige bzw. fehlende Eingaben entdeckt. Bitte prüfen Sie die rot markierten Felder.', { + variant: 'error', + }) + return + } + + const regionId = 1 // TODO: Add a mechanism to retrieve the regionId + + addBlueEakApplication({ + variables: { + regionId, // TODO: Add a mechanism to retrieve the regionId + application: application.value, + }, + }) + } + + return ( +
+
+

Blaue Ehrenamtskarte beantragen

+
{ + e.preventDefault() + submit() + }}> + + + setState(() => ApplicationForm.initialState)} /> + + + +
+
+ ) +} + +const ApplyApp = () => ( + + + +) + +export default ApplyApp diff --git a/administration/src/application/components/ConfirmDialog.tsx b/administration/src/application/components/ConfirmDialog.tsx new file mode 100644 index 000000000..8db1f6fa1 --- /dev/null +++ b/administration/src/application/components/ConfirmDialog.tsx @@ -0,0 +1,38 @@ +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material' + +const ConfirmDialog = ({ + open, + onUpdateOpen, + title, + content, + onConfirm, + confirmButtonText = 'Bestätigen', +}: { + open: boolean + onUpdateOpen: (open: boolean) => void + title: string + content: string + onConfirm: () => void + confirmButtonText?: string +}) => { + return ( + onUpdateOpen(false)}> + {title} + + {content} + + + + + + + ) +} + +export default ConfirmDialog diff --git a/administration/src/application/components/DiscardAllInputsButton.tsx b/administration/src/application/components/DiscardAllInputsButton.tsx new file mode 100644 index 000000000..07a5ea81b --- /dev/null +++ b/administration/src/application/components/DiscardAllInputsButton.tsx @@ -0,0 +1,24 @@ +import { Delete } from '@mui/icons-material' +import { Button } from '@mui/material' +import { useState } from 'react' +import ConfirmDialog from './ConfirmDialog' + +const DiscardAllInputsButton = ({ discardAll }: { discardAll: () => void }) => { + const [dialogOpen, setDialogOpen] = useState(false) + return ( + <> + + + + ) +} + +export default DiscardAllInputsButton diff --git a/administration/src/application/components/SwitchDisplay.tsx b/administration/src/application/components/SwitchDisplay.tsx new file mode 100644 index 000000000..4114343a6 --- /dev/null +++ b/administration/src/application/components/SwitchDisplay.tsx @@ -0,0 +1,15 @@ +import React from 'react' + +const SwitchDisplay = ({ children, value }: { children: { [key: string]: React.ReactNode }; value: string | null }) => { + return ( + <> + {Object.entries(children).map(([key, element]) => ( +
+ {element} +
+ ))} + + ) +} + +export default SwitchDisplay diff --git a/administration/src/application/components/forms/AddressForm.tsx b/administration/src/application/components/forms/AddressForm.tsx new file mode 100644 index 000000000..d527b41d1 --- /dev/null +++ b/administration/src/application/components/forms/AddressForm.tsx @@ -0,0 +1,89 @@ +import { AddressInput } from '../../../generated/graphql' +import ShortTextForm, { ShortTextFormState } from '../primitive-inputs/ShortTextForm' +import { useUpdateStateCallback } from '../../useUpdateStateCallback' +import { Form } from '../../FormType' + +export type AddressFormState = { + street: ShortTextFormState + houseNumber: ShortTextFormState + location: ShortTextFormState + postalCode: ShortTextFormState +} +type ValidatedInput = AddressInput +type Options = {} +type AdditionalProps = {} +const AddressForm: Form = { + initialState: { + street: ShortTextForm.initialState, + houseNumber: ShortTextForm.initialState, + location: ShortTextForm.initialState, + postalCode: ShortTextForm.initialState, + }, + getArrayBufferKeys: state => [ + ...ShortTextForm.getArrayBufferKeys(state.street), + ...ShortTextForm.getArrayBufferKeys(state.houseNumber), + ...ShortTextForm.getArrayBufferKeys(state.location), + ...ShortTextForm.getArrayBufferKeys(state.postalCode), + ], + getValidatedInput: state => { + const street = ShortTextForm.getValidatedInput(state.street) + const houseNumber = ShortTextForm.getValidatedInput(state.houseNumber) + const location = ShortTextForm.getValidatedInput(state.location) + const postalCode = ShortTextForm.getValidatedInput(state.postalCode) + if ( + street.type === 'error' || + houseNumber.type === 'error' || + location.type === 'error' || + postalCode.type === 'error' + ) + return { type: 'error' } + return { + type: 'valid', + value: { + street: street.value, + houseNumber: houseNumber.value, + location: location.value, + postalCode: postalCode.value, + }, + } + }, + Component: ({ state, setState }) => ( + <> +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ + ), +} + +export default AddressForm diff --git a/administration/src/application/components/forms/ApplicationForm.tsx b/administration/src/application/components/forms/ApplicationForm.tsx new file mode 100644 index 000000000..8f46c64b0 --- /dev/null +++ b/administration/src/application/components/forms/ApplicationForm.tsx @@ -0,0 +1,98 @@ +import { SetState, useUpdateStateCallback } from '../../useUpdateStateCallback' +import { ApplicationType, BlueCardApplicationInput, BlueCardEntitlementType } from '../../../generated/graphql' +import SwitchDisplay from '../SwitchDisplay' +import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material' +import { Form } from '../../FormType' +import StandardEntitlementForm, { StandardEntitlementFormState } from './StandardEntitlementForm' +import PersonalDataForm, { PersonalDataFormState } from './PersonalDataForm' + +const EntitlementTypeInput = ({ + state, + setState, +}: { + state: BlueCardEntitlementType | null + setState: SetState +}) => { + return ( + + In den folgenden Fällen können Sie eine blaue Ehrenamtskarte beantragen: + setState(() => e.target.value as BlueCardEntitlementType)}> + } + /> + + + ) +} + +export type ApplicationFormState = { + entitlementType: BlueCardEntitlementType | null + standardEntitlement: StandardEntitlementFormState + personalData: PersonalDataFormState +} +type ValidatedInput = BlueCardApplicationInput +type Options = {} +type AdditionalProps = {} +const ApplicationForm: Form = { + initialState: { + entitlementType: null, + standardEntitlement: StandardEntitlementForm.initialState, + personalData: PersonalDataForm.initialState, + }, + getArrayBufferKeys: state => [ + ...StandardEntitlementForm.getArrayBufferKeys(state.standardEntitlement), + ...PersonalDataForm.getArrayBufferKeys(state.personalData), + ], + getValidatedInput: state => { + const personalData = PersonalDataForm.getValidatedInput(state.personalData) + if (state.entitlementType === null || personalData.type === 'error') return { type: 'error' } + switch (state.entitlementType) { + case BlueCardEntitlementType.Standard: + const workAtOrganizations = StandardEntitlementForm.getValidatedInput(state.standardEntitlement) + if (workAtOrganizations.type === 'error') return { type: 'error' } + return { + type: 'valid', + value: { + entitlement: { + entitlementType: state.entitlementType, + workAtOrganizations: workAtOrganizations.value, + }, + personalData: personalData.value, + hasAcceptedPrivacyPolicy: true, // TODO: Add a corresponding field + applicationType: ApplicationType.FirstApplication, // TODO: Add a corresponding field + givenInformationIsCorrectAndComplete: true, // TODO: Add a corresponding field + }, + } + default: + throw Error('Not yet implemented.') + } + }, + Component: ({ state, setState }) => ( + <> + + + {{ + [BlueCardEntitlementType.Standard]: ( + + ), + [BlueCardEntitlementType.Juleica]: null, + [BlueCardEntitlementType.Service]: null, + }} + + + + ), +} + +export default ApplicationForm diff --git a/administration/src/application/components/forms/OrganizationForm.tsx b/administration/src/application/components/forms/OrganizationForm.tsx new file mode 100644 index 000000000..50bdb2c49 --- /dev/null +++ b/administration/src/application/components/forms/OrganizationForm.tsx @@ -0,0 +1,118 @@ +import { OrganizationInput } from '../../../generated/graphql' +import { useUpdateStateCallback } from '../../useUpdateStateCallback' +import { Form } from '../../FormType' +import ShortTextForm, { ShortTextFormState } from '../primitive-inputs/ShortTextForm' +import AddressForm, { AddressFormState } from './AddressForm' +import SelectForm, { SelectFormState } from '../primitive-inputs/SelectForm' +import EmailForm, { EmailFormState } from '../primitive-inputs/EmailForm' + +const organizationCategoryOptions = { + items: [ + 'Soziales/Jugend/Senioren', + 'Tierschutz', + 'Sport', + 'Bildung', + 'Umwelt-/Naturschutz', + 'Kultur', + 'Gesundheit', + 'Katastrophenschutz/Feuerwehr/Rettungsdienst', + 'Kirchen', + 'Andere', + ], +} + +export type OrganizationFormState = { + name: ShortTextFormState + address: AddressFormState + category: SelectFormState + contactName: ShortTextFormState + contactEmail: EmailFormState + contactPhone: ShortTextFormState +} +type ValidatedInput = OrganizationInput +type Options = {} +type AdditionalProps = {} +const OrganizationForm: Form = { + initialState: { + name: ShortTextForm.initialState, + address: AddressForm.initialState, + category: SelectForm.initialState, + contactName: ShortTextForm.initialState, + contactEmail: EmailForm.initialState, + contactPhone: ShortTextForm.initialState, + }, + getArrayBufferKeys: state => [ + ...ShortTextForm.getArrayBufferKeys(state.name), + ...AddressForm.getArrayBufferKeys(state.address), + ...SelectForm.getArrayBufferKeys(state.category), + ...ShortTextForm.getArrayBufferKeys(state.contactName), + ...EmailForm.getArrayBufferKeys(state.contactEmail), + ...ShortTextForm.getArrayBufferKeys(state.contactPhone), + ], + getValidatedInput: state => { + const name = ShortTextForm.getValidatedInput(state.name) + const address = AddressForm.getValidatedInput(state.address) + const category = SelectForm.getValidatedInput(state.category, organizationCategoryOptions) + const contactName = ShortTextForm.getValidatedInput(state.contactName) + const contactEmail = EmailForm.getValidatedInput(state.contactEmail) + const contactPhone = ShortTextForm.getValidatedInput(state.contactPhone) + if ( + name.type === 'error' || + address.type === 'error' || + category.type === 'error' || + contactName.type === 'error' || + contactEmail.type === 'error' || + contactPhone.type === 'error' + ) + return { type: 'error' } + return { + type: 'valid', + value: { + name: name.value, + address: address.value, + category: category.value, + contact: { + name: contactName.value, + email: contactEmail.value, + telephone: contactPhone.value, + hasGivenPermission: true, // TODO: Add a field for this. + }, + }, + } + }, + Component: ({ state, setState }) => ( + <> +

Angaben zur Organisation

+ + + +

Kontaktperson der Organisation

+ + + + + ), +} + +export default OrganizationForm diff --git a/administration/src/application/components/forms/PersonalDataForm.tsx b/administration/src/application/components/forms/PersonalDataForm.tsx new file mode 100644 index 000000000..bbe1dde97 --- /dev/null +++ b/administration/src/application/components/forms/PersonalDataForm.tsx @@ -0,0 +1,107 @@ +import { PersonalDataInput } from '../../../generated/graphql' +import AddressForm, { AddressFormState } from './AddressForm' +import EmailForm, { EmailFormState } from '../primitive-inputs/EmailForm' +import ShortTextForm, { ShortTextFormState } from '../primitive-inputs/ShortTextForm' +import DateForm, { DateFormState } from '../primitive-inputs/DateForm' +import { useUpdateStateCallback } from '../../useUpdateStateCallback' +import { Form } from '../../FormType' + +export type PersonalDataFormState = { + forenames: ShortTextFormState + surname: ShortTextFormState + addressFormState: AddressFormState + emailAddress: EmailFormState + telephone: ShortTextFormState + dateOfBirth: DateFormState +} +type ValidatedInput = PersonalDataInput +type Options = {} +type AdditionalProps = {} +const PersonalDataForm: Form = { + initialState: { + forenames: ShortTextForm.initialState, + surname: ShortTextForm.initialState, + addressFormState: AddressForm.initialState, + emailAddress: EmailForm.initialState, + telephone: ShortTextForm.initialState, + dateOfBirth: DateForm.initialState, + }, + getArrayBufferKeys: state => [ + ...ShortTextForm.getArrayBufferKeys(state.forenames), + ...ShortTextForm.getArrayBufferKeys(state.surname), + ...AddressForm.getArrayBufferKeys(state.addressFormState), + ...EmailForm.getArrayBufferKeys(state.emailAddress), + ...ShortTextForm.getArrayBufferKeys(state.telephone), + ...DateForm.getArrayBufferKeys(state.dateOfBirth), + ], + getValidatedInput: state => { + const forenames = ShortTextForm.getValidatedInput(state.forenames) + const surname = ShortTextForm.getValidatedInput(state.surname) + const address = AddressForm.getValidatedInput(state.addressFormState) + const emailAddress = EmailForm.getValidatedInput(state.emailAddress) + const telephone = ShortTextForm.getValidatedInput(state.telephone) + const dateOfBirth = DateForm.getValidatedInput(state.dateOfBirth) + if ( + forenames.type === 'error' || + surname.type === 'error' || + address.type === 'error' || + emailAddress.type === 'error' || + telephone.type === 'error' || + dateOfBirth.type === 'error' + ) + return { type: 'error' } + return { + type: 'valid', + value: { + forenames: forenames.value, + surname: surname.value, + address: address.value, + emailAddress: emailAddress.value, + telephone: telephone.value, + dateOfBirth: dateOfBirth.value, + }, + } + }, + Component: ({ state, setState }) => ( + <> +

Persönliche Angaben

+
+
+ +
+
+ +
+
+ + + + + + ), +} + +export default PersonalDataForm diff --git a/administration/src/application/components/forms/StandardEntitlementForm.tsx b/administration/src/application/components/forms/StandardEntitlementForm.tsx new file mode 100644 index 000000000..377db1873 --- /dev/null +++ b/administration/src/application/components/forms/StandardEntitlementForm.tsx @@ -0,0 +1,94 @@ +import { Alert, Button } from '@mui/material' +import WorkAtOrganizationForm, { WorkAtOrganizationFormState } from './WorkAtOrganizationForm' +import { WorkAtOrganizationInput } from '../../../generated/graphql' +import { SetState } from '../../useUpdateStateCallback' +import { useCallback, useMemo } from 'react' +import { Form } from '../../FormType' + +const WorkAtOrganizationFormHelper = ({ + listKey, + setStateByKey, + ...otherProps +}: { + state: WorkAtOrganizationFormState + listKey: number + setStateByKey: (key: number) => SetState + onDelete?: () => void +}) => { + const setState = useMemo(() => setStateByKey(listKey), [setStateByKey, listKey]) + return +} + +function replaceAt(array: T[], index: number, newItem: T): T[] { + const newArray = [...array] + newArray[index] = newItem + return newArray +} + +function removeAt(array: T[], index: number): T[] { + const newArray = [...array] + newArray.splice(index, 1) + return newArray +} + +export type StandardEntitlementFormState = { key: number; value: WorkAtOrganizationFormState }[] +type ValidatedInput = WorkAtOrganizationInput[] +type Options = {} +type AdditionalProps = {} +const StandardEntitlementForm: Form = { + initialState: [{ key: 0, value: WorkAtOrganizationForm.initialState }], + getArrayBufferKeys: state => state.map(({ value }) => WorkAtOrganizationForm.getArrayBufferKeys(value)).flat(), + getValidatedInput: state => { + const validationResults = state.map(({ value }) => WorkAtOrganizationForm.getValidatedInput(value)) + if (validationResults.some(({ type }) => type === 'error')) { + return { type: 'error' } + } + return { + type: 'valid', + value: validationResults.map(x => { + if (x.type !== 'valid') { + throw Error('Found an invalid entry despite previous validity check.') + } + return x.value + }), + } + }, + Component: ({ state, setState }) => { + const addActivity = () => + setState(state => { + const newKey = Math.max(...state.map(({ key }) => key), 0) + 1 + return [...state, { key: newKey, value: WorkAtOrganizationForm.initialState }] + }) + + const setStateByKey: (key: number) => SetState = useCallback( + key => update => + setState(state => { + const index = state.findIndex(element => element.key === key) + return replaceAt(state, index, { key, value: update(state[index].value) }) + }), + [setState] + ) + + return ( + <> +

Ehrenamtliche Tätigkeit(en)

+ {state.map(({ key, value }, index) => ( + setState(state => removeAt(state, index))} + setStateByKey={setStateByKey} + /> + ))} + {state.length < 10 ? ( + + ) : ( + Maximale Anzahl an Tätigkeiten erreicht. + )} + + ) + }, +} + +export default StandardEntitlementForm diff --git a/administration/src/application/components/forms/WorkAtOrganizationForm.tsx b/administration/src/application/components/forms/WorkAtOrganizationForm.tsx new file mode 100644 index 000000000..eeb864345 --- /dev/null +++ b/administration/src/application/components/forms/WorkAtOrganizationForm.tsx @@ -0,0 +1,149 @@ +import { Card, CardContent, Checkbox, FormControlLabel, FormGroup, IconButton, Tooltip } from '@mui/material' +import DeleteIcon from '@mui/icons-material/Delete' +import { AmountOfWorkUnit, WorkAtOrganizationInput } from '../../../generated/graphql' +import { useState } from 'react' +import ConfirmDialog from '../ConfirmDialog' +import { useUpdateStateCallback } from '../../useUpdateStateCallback' +import { Form } from '../../FormType' +import OrganizationForm, { OrganizationFormState } from './OrganizationForm' +import DateForm, { DateFormState } from '../primitive-inputs/DateForm' +import ShortTextForm, { ShortTextFormState } from '../primitive-inputs/ShortTextForm' +import NumberForm, { NumberFormState } from '../primitive-inputs/NumberForm' +import FileInputForm, { FILE_SIZE_LIMIT_MEGA_BYTES, FileInputFormState } from '../primitive-inputs/FileInputForm' + +const DeleteActivityButton = ({ onDelete }: { onDelete?: () => void }) => { + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + return onDelete === undefined ? null : ( + <> + + setDeleteDialogOpen(true)} + style={{ position: 'absolute', top: '0', right: '0' }} + aria-label='Aktivität löschen'> + + + + + + ) +} + +const amountOfWorkOptions = { min: 0, max: 100 } + +export type WorkAtOrganizationFormState = { + organization: OrganizationFormState + amountOfWork: NumberFormState + activeSince: DateFormState + payment: boolean + responsibility: ShortTextFormState + certificate: FileInputFormState +} +type ValidatedInput = WorkAtOrganizationInput +type Options = {} +type AdditionalProps = { onDelete?: () => void } +const WorkAtOrganizationForm: Form = { + initialState: { + organization: OrganizationForm.initialState, + amountOfWork: NumberForm.initialState, + activeSince: DateForm.initialState, + payment: false, + responsibility: ShortTextForm.initialState, + certificate: FileInputForm.initialState, + }, + getArrayBufferKeys: state => [ + ...OrganizationForm.getArrayBufferKeys(state.organization), + ...NumberForm.getArrayBufferKeys(state.amountOfWork), + ...DateForm.getArrayBufferKeys(state.activeSince), + ...ShortTextForm.getArrayBufferKeys(state.responsibility), + ...FileInputForm.getArrayBufferKeys(state.certificate), + ], + getValidatedInput: state => { + const organization = OrganizationForm.getValidatedInput(state.organization) + const amountOfWork = NumberForm.getValidatedInput(state.amountOfWork, amountOfWorkOptions) + const activeSince = DateForm.getValidatedInput(state.activeSince) + const responsibility = ShortTextForm.getValidatedInput(state.responsibility) + const certificate = FileInputForm.getValidatedInput(state.certificate) + if ( + organization.type === 'error' || + amountOfWork.type === 'error' || + activeSince.type === 'error' || + responsibility.type === 'error' || + certificate.type === 'error' + ) + return { type: 'error' } + return { + type: 'valid', + value: { + organization: organization.value, + amountOfWork: amountOfWork.value, + amountOfWorkUnit: AmountOfWorkUnit.HoursPerWeek, + workSinceDate: activeSince.value, + responsibility: responsibility.value, + certificate: certificate.value, + payment: state.payment, + }, + } + }, + Component: ({ state, setState, onDelete }) => ( + + + + +

Angaben zur Tätigkeit

+ +
+
+ +
+
+ +
+
+ + setState(state => ({ ...state, payment: e.target.checked }))} + /> + } + label='Für diese ehrenamtliche Tätigkeit wurde eine Aufwandsentschädigung gewährt.' + /> + +

Tätigkeitsnachweis

+

+ Hängen Sie hier bitte einen eingescannten oder abfotografierten Tätigkeitsnachweis an. Dieser darf maximal{' '} + {FILE_SIZE_LIMIT_MEGA_BYTES} MB groß sein. Wählen Sie eine Datei im JPG, PNG oder PDF Format. +

+ +
+
+ ), +} + +export default WorkAtOrganizationForm diff --git a/administration/src/application/components/primitive-inputs/DateForm.tsx b/administration/src/application/components/primitive-inputs/DateForm.tsx new file mode 100644 index 000000000..ba365f584 --- /dev/null +++ b/administration/src/application/components/primitive-inputs/DateForm.tsx @@ -0,0 +1,45 @@ +import { TextField } from '@mui/material' +import { useState } from 'react' +import { Form } from '../../FormType' + +export type DateFormState = { type: 'DateForm'; value: string } +type ValidatedInput = string +type Options = {} +type AdditionalProps = { label: string; minWidth?: number } +const DateForm: Form = { + initialState: { type: 'DateForm', value: '' }, + getArrayBufferKeys: () => [], + getValidatedInput: ({ value }) => { + if (value === '') return { type: 'error', message: 'Feld ist erforderlich.' } + const date = Date.parse(value) + if (isNaN(date)) { + return { type: 'error', message: 'Eingabe ist kein gültiges Datum.' } + } + return { type: 'valid', value: new Date(date).toString() } + }, + Component: ({ state, setState, label, minWidth = 100 }) => { + const [touched, setTouched] = useState(false) + const validationResult = DateForm.getValidatedInput(state) + + const isInvalid = validationResult.type === 'error' + + return ( + setTouched(true)} + onChange={e => setState(() => ({ type: 'DateForm', value: e.target.value }))} + helperText={touched && isInvalid ? validationResult.message : ''} + /> + ) + }, +} + +export default DateForm diff --git a/administration/src/application/components/primitive-inputs/EmailForm.tsx b/administration/src/application/components/primitive-inputs/EmailForm.tsx new file mode 100644 index 000000000..6c4b0d858 --- /dev/null +++ b/administration/src/application/components/primitive-inputs/EmailForm.tsx @@ -0,0 +1,40 @@ +import { TextField } from '@mui/material' +import { useState } from 'react' +import { Form } from '../../FormType' + +export type EmailFormState = { type: 'EmailForm'; value: string } +type ValidatedInput = string +type Options = {} +type AdditionalProps = { label: string; minWidth?: number } +const EmailForm: Form = { + initialState: { type: 'EmailForm', value: '' }, + getArrayBufferKeys: () => [], + getValidatedInput: ({ value }) => { + if (value === '') return { type: 'error', message: 'Feld ist erforderlich.' } + return { type: 'valid', value } + }, + Component: ({ state, setState, label, minWidth = 100 }) => { + const [touched, setTouched] = useState(false) + const validationResult = EmailForm.getValidatedInput(state) + + const isInvalid = validationResult.type === 'error' + + return ( + setTouched(true)} + onChange={e => setState(() => ({ type: 'EmailForm', value: e.target.value }))} + helperText={touched && isInvalid ? validationResult.message : ''} + /> + ) + }, +} + +export default EmailForm diff --git a/administration/src/application/components/primitive-inputs/FileInputForm.tsx b/administration/src/application/components/primitive-inputs/FileInputForm.tsx new file mode 100644 index 000000000..2d8fdccf6 --- /dev/null +++ b/administration/src/application/components/primitive-inputs/FileInputForm.tsx @@ -0,0 +1,101 @@ +import { AttachFile, Attachment } from '@mui/icons-material' +import { Button, Chip, FormHelperText } from '@mui/material' +import { ChangeEventHandler, useEffect, useRef } from 'react' +import { AttachmentInput } from '../../../generated/graphql' +import globalArrayBuffersManager from '../../globalArrayBuffersManager' +import { Form } from '../../FormType' +import { useSnackbar } from 'notistack' + +const defaultExtensionsByMIMEType = { + 'application/pdf': '.pdf', + 'image/png': '.png', + 'image/jpeg': '.jpg', +} + +export const FILE_SIZE_LIMIT_MEGA_BYTES = 5 +const FILE_SIZE_LIMIT_BYTES = FILE_SIZE_LIMIT_MEGA_BYTES * 1000 * 1000 + +const FileInputButton = ({ onChange, label }: { onChange: ChangeEventHandler; label: string }) => { + const inputRef = useRef(null) + return ( + <> + + + + ) +} + +export type FileInputFormState = { + MIMEType: keyof typeof defaultExtensionsByMIMEType + arrayBufferKey: number + filename: string +} | null +type ValidatedInput = AttachmentInput +type Options = {} +type AdditionalProps = {} +const FileInputForm: Form = { + initialState: null, + getArrayBufferKeys: state => (state === null ? [] : [state.arrayBufferKey]), + getValidatedInput: state => { + if (state === null) return { type: 'error', message: 'Feld ist erforderlich.' } + if (!globalArrayBuffersManager.has(state.arrayBufferKey)) return { type: 'error' } + const arrayBuffer = globalArrayBuffersManager.getArrayBufferByKey(state.arrayBufferKey) + return { + type: 'valid', + value: { + fileName: state.filename, + data: new File([arrayBuffer], state.filename, { type: state.MIMEType }), + }, + } + }, + Component: ({ state, setState }) => { + const { enqueueSnackbar } = useSnackbar() + const validationResult = FileInputForm.getValidatedInput(state) + const onInputChange: ChangeEventHandler = async e => { + const file = e.target.files![0] + if (!(file.type in defaultExtensionsByMIMEType)) { + enqueueSnackbar('Die gewählte Datei hat einen unzulässigen Dateityp.', { variant: 'error' }) + e.target.value = '' + return + } + if (file.size > FILE_SIZE_LIMIT_BYTES) { + enqueueSnackbar( + `Die gewählte Datei ist zu groß. Die maximale Dateigröße beträgt ${FILE_SIZE_LIMIT_MEGA_BYTES}MB.`, + { variant: 'error' } + ) + e.target.value = '' + return + } + const fileType = file.type as keyof typeof defaultExtensionsByMIMEType + const name = 'file' + defaultExtensionsByMIMEType[fileType] + const arrayBuffer = await file.arrayBuffer() + const key = globalArrayBuffersManager.addArrayBuffer(arrayBuffer) + setState(() => ({ MIMEType: fileType, filename: name, arrayBufferKey: key })) + } + + useEffect(() => { + if (state === null) { + return + } + // If the arrayBufferManager doesn't have the specified key, let user reenter a File. + if (!globalArrayBuffersManager.has(state.arrayBufferKey)) { + setState(() => null) + } + }, [state, setState]) + + if (state === null) { + return ( + <> + + {validationResult.type === 'error' ? {validationResult.message} : null} + + ) + } + + return } onDelete={() => setState(() => null)} /> + }, +} + +export default FileInputForm diff --git a/administration/src/application/components/primitive-inputs/NumberForm.tsx b/administration/src/application/components/primitive-inputs/NumberForm.tsx new file mode 100644 index 000000000..1dca6da7d --- /dev/null +++ b/administration/src/application/components/primitive-inputs/NumberForm.tsx @@ -0,0 +1,43 @@ +import { TextField } from '@mui/material' +import { useState } from 'react' +import { Form } from '../../FormType' + +export type NumberFormState = { type: 'NumberForm'; value: string } +type ValidatedInput = number +type Options = { min: number; max: number } +type AdditionalProps = { label: string; minWidth?: number } +const NumberForm: Form = { + initialState: { type: 'NumberForm', value: '' }, + getArrayBufferKeys: () => [], + getValidatedInput: ({ value }, options) => { + const number = parseFloat(value) + if (isNaN(number)) return { type: 'error', message: 'Eingabe ist keine Zahl.' } + else if (number < options.min || number > options.max) + return { type: 'error', message: `Wert muss zwischen ${options.min} und ${options.max} liegen.` } + return { type: 'valid', value: number } + }, + Component: ({ state, setState, label, options, minWidth = 100 }) => { + const [touched, setTouched] = useState(false) + const validationResult = NumberForm.getValidatedInput(state, options) + const isInvalid = validationResult.type === 'error' + + return ( + setTouched(true)} + onChange={e => setState(() => ({ type: 'NumberForm', value: e.target.value }))} + helperText={touched && isInvalid ? validationResult.message : ''} + inputProps={{ inputMode: 'numeric', min: options.min, max: options.max }} + /> + ) + }, +} + +export default NumberForm diff --git a/administration/src/application/components/primitive-inputs/SelectForm.tsx b/administration/src/application/components/primitive-inputs/SelectForm.tsx new file mode 100644 index 000000000..8d204b1ad --- /dev/null +++ b/administration/src/application/components/primitive-inputs/SelectForm.tsx @@ -0,0 +1,46 @@ +import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from '@mui/material' +import { useState } from 'react' +import { Form } from '../../FormType' + +export type SelectFormState = string +type ValidatedInput = string +type Options = { items: string[] } +type AdditionalProps = { label: string } +const SelectForm: Form = { + initialState: '', + getArrayBufferKeys: () => [], + getValidatedInput: (state, options) => { + if (state.length === 0) return { type: 'error', message: 'Feld ist erforderlich.' } + if (!options.items.includes(state)) + return { + type: 'error', + message: `Wert muss einer der auswählbaren Optionen entsprechen.`, + } + return { type: 'valid', value: state } + }, + Component: ({ state, setState, label, options }) => { + const [touched, setTouched] = useState(false) + const validationResult = SelectForm.getValidatedInput(state, options) + const isInvalid = validationResult.type === 'error' + + return ( + + {label} + + {!touched || !isInvalid ? null : {validationResult.message}} + + ) + }, +} + +export default SelectForm diff --git a/administration/src/application/components/primitive-inputs/ShortTextForm.tsx b/administration/src/application/components/primitive-inputs/ShortTextForm.tsx new file mode 100644 index 000000000..680e9e6f6 --- /dev/null +++ b/administration/src/application/components/primitive-inputs/ShortTextForm.tsx @@ -0,0 +1,48 @@ +import { TextField } from '@mui/material' +import { useState } from 'react' +import { Form } from '../../FormType' + +const MAX_CHARACTERS = 300 + +export type ShortTextFormState = { + type: 'ShortText' + value: string +} +type ValidatedInput = string +type Options = {} +type AdditionalProps = { label: string; minWidth?: number } +const ShortTextForm: Form = { + initialState: { type: 'ShortText', value: '' }, + getArrayBufferKeys: () => [], + getValidatedInput: ({ value }) => { + if (value.length === 0) return { type: 'error', message: 'Feld ist erforderlich.' } + if (value.length > MAX_CHARACTERS) + return { + type: 'error', + message: `Text überschreitet die erlaubten ${MAX_CHARACTERS} Zeichen.`, + } + return { type: 'valid', value } + }, + Component: ({ state, setState, label, minWidth = 200 }) => { + const [touched, setTouched] = useState(false) + const validationResult = ShortTextForm.getValidatedInput(state) + const isInvalid = validationResult.type === 'error' + + return ( + setTouched(true)} + value={state.value} + onChange={e => setState(() => ({ type: 'ShortText', value: e.target.value }))} + helperText={touched && isInvalid ? validationResult.message : ''} + /> + ) + }, +} + +export default ShortTextForm diff --git a/administration/src/application/globalArrayBuffersManager.ts b/administration/src/application/globalArrayBuffersManager.ts new file mode 100644 index 000000000..5e5633f40 --- /dev/null +++ b/administration/src/application/globalArrayBuffersManager.ts @@ -0,0 +1,70 @@ +import localforage from 'localforage' +import { useEffect, useRef, useState } from 'react' + +const globalArrayBuffersKey = 'array-buffers' + +class ArrayBuffersManager { + private arrayBuffers: { key: number; value: ArrayBuffer }[] = [] + + async initialize() { + const arrayBuffers = await localforage.getItem<{ key: number; value: ArrayBuffer }[]>(globalArrayBuffersKey) + if (arrayBuffers !== null) { + this.arrayBuffers = arrayBuffers + } + } + + getArrayBufferByKey(key: number): ArrayBuffer { + const element = this.arrayBuffers.find(({ key: elementKey }) => key === elementKey) + if (element === undefined) { + throw Error('Invalid index') + } + return element.value + } + + addArrayBuffer(arrayBuffer: ArrayBuffer): number { + const newKey = Math.max(...this.arrayBuffers.map(({ key }) => key), 0) + 1 + this.arrayBuffers.push({ key: newKey, value: arrayBuffer }) + localforage.setItem(globalArrayBuffersKey, this.arrayBuffers) + return newKey + } + + has(key: number): boolean { + return this.arrayBuffers.find(({ key: elementKey }) => key === elementKey) !== undefined + } + + clearAllExcept(except: Set) { + this.arrayBuffers = this.arrayBuffers.filter(({ key }) => except.has(key)) + localforage.setItem(globalArrayBuffersKey, this.arrayBuffers) + } +} + +const globalArrayBuffersManager = new ArrayBuffersManager() + +export const useInitializeGlobalArrayBuffersManager = () => { + const [initialized, setInitialized] = useState(false) + useEffect(() => { + globalArrayBuffersManager.initialize().finally(() => setInitialized(true)) + }, [setInitialized]) + return initialized +} + +export const useGarbageCollectArrayBuffers = (getUsedArrayBufferKeys: (() => number[]) | null) => { + const getUsedArrayBufferKeysRef = useRef(getUsedArrayBufferKeys) + + useEffect(() => { + getUsedArrayBufferKeysRef.current = getUsedArrayBufferKeys + }, [getUsedArrayBufferKeys]) + + useEffect(() => { + const interval = setInterval(() => { + // Collect Garbage + const getKeys = getUsedArrayBufferKeysRef.current + if (getKeys !== null) { + globalArrayBuffersManager.clearAllExcept(new Set(getKeys())) + } + }, 2000) + return () => clearInterval(interval) + }) +} + +export default globalArrayBuffersManager diff --git a/administration/src/application/useLocallyStoredState.ts b/administration/src/application/useLocallyStoredState.ts new file mode 100644 index 000000000..4d2877644 --- /dev/null +++ b/administration/src/application/useLocallyStoredState.ts @@ -0,0 +1,47 @@ +import localforage from 'localforage' +import { useCallback, useEffect, useRef, useState } from 'react' +import { SetState } from './useUpdateStateCallback' + +function useLocallyStoredState(initialState: T, storageKey: string): [T | null, SetState] { + const [state, setState] = useState(initialState) + const stateRef = useRef(state) + const [loading, setLoading] = useState(true) + + const setStateAndRef = useCallback((update: (oldState: T) => T) => { + setState(state => { + const newState = update(state) + stateRef.current = newState + return newState + }) + }, []) + + useEffect(() => { + localforage + .getItem(storageKey) + .then(storedValue => { + if (storedValue !== null) { + setStateAndRef(() => storedValue) + } + }) + .finally(() => setLoading(false)) + }, [storageKey, setStateAndRef]) + + useEffect(() => { + // Auto-save every 2 seconds unless we're still loading the state. + if (loading) { + return + } + let lastState: T | null = null + const interval = setInterval(() => { + if (lastState !== stateRef.current) { + localforage.setItem(storageKey, stateRef.current) + lastState = stateRef.current + } + }, 2000) + return () => clearInterval(interval) + }, [loading, storageKey]) + + return [loading ? null : state, setStateAndRef] +} + +export default useLocallyStoredState diff --git a/administration/src/application/useUpdateStateCallback.tsx b/administration/src/application/useUpdateStateCallback.tsx new file mode 100644 index 000000000..4d8f70d46 --- /dev/null +++ b/administration/src/application/useUpdateStateCallback.tsx @@ -0,0 +1,9 @@ +import { useCallback } from 'react' + +export type Update = (oldState: T) => T + +export type SetState = (update: Update) => void + +export function useUpdateStateCallback(setState: SetState, key: S): SetState { + return useCallback(update => setState(state => ({ ...state, [key]: update(state[key]) })), [setState, key]) +} diff --git a/administration/src/graphql/applications/apply.graphql b/administration/src/graphql/applications/apply.graphql new file mode 100644 index 000000000..ddfd42ba0 --- /dev/null +++ b/administration/src/graphql/applications/apply.graphql @@ -0,0 +1,3 @@ +mutation addBlueEakApplication($regionId: Int!, $application: BlueCardApplicationInput!) { + result: addBlueEakApplication(regionId: $regionId, application: $application) +} diff --git a/backend/src/main/kotlin/app/ehrenamtskarte/backend/application/webservice/EakApplicationMutationService.kt b/backend/src/main/kotlin/app/ehrenamtskarte/backend/application/webservice/EakApplicationMutationService.kt index e0d8e65a1..c24746d8a 100644 --- a/backend/src/main/kotlin/app/ehrenamtskarte/backend/application/webservice/EakApplicationMutationService.kt +++ b/backend/src/main/kotlin/app/ehrenamtskarte/backend/application/webservice/EakApplicationMutationService.kt @@ -10,6 +10,7 @@ import app.ehrenamtskarte.backend.common.webservice.GraphQLContext import app.ehrenamtskarte.backend.common.webservice.UnauthorizedException import com.expediagroup.graphql.generator.annotations.GraphQLDescription import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.transactions.transaction @Suppress("unused") class EakApplicationMutationService { @@ -20,7 +21,7 @@ class EakApplicationMutationService { application: BlueCardApplication, dfe: DataFetchingEnvironment ): Boolean { - val context = dfe.getLocalContext() + val context = dfe.getContext() ApplicationRepository.addApplication( regionId, application, @@ -51,18 +52,20 @@ class EakApplicationMutationService { applicationId: Int, dfe: DataFetchingEnvironment ): Boolean { - val context = dfe.getLocalContext() + val context = dfe.getContext() val jwtPayload = context.enforceSignedIn() - val application = ApplicationEntity.findById(applicationId) ?: throw UnauthorizedException() - // We throw an UnauthorizedException here, as we do not know whether there was an application with id - // `applicationId` and whether this application was contained in the user's project & region. + return transaction { + val application = ApplicationEntity.findById(applicationId) ?: throw UnauthorizedException() + // We throw an UnauthorizedException here, as we do not know whether there was an application with id + // `applicationId` and whether this application was contained in the user's project & region. - val user = AdministratorEntity.findById(jwtPayload.userId) - if (!mayDeleteApplicationsInRegion(user, application.regionId.value)) { - throw UnauthorizedException() - } + val user = AdministratorEntity.findById(jwtPayload.userId) + if (!mayDeleteApplicationsInRegion(user, application.regionId.value)) { + throw UnauthorizedException() + } - return ApplicationRepository.delete(applicationId, context) + ApplicationRepository.delete(applicationId, context) + } } } diff --git a/backend/src/main/kotlin/app/ehrenamtskarte/backend/application/webservice/EakApplicationQueryService.kt b/backend/src/main/kotlin/app/ehrenamtskarte/backend/application/webservice/EakApplicationQueryService.kt index 8708aa202..b8ec4af15 100644 --- a/backend/src/main/kotlin/app/ehrenamtskarte/backend/application/webservice/EakApplicationQueryService.kt +++ b/backend/src/main/kotlin/app/ehrenamtskarte/backend/application/webservice/EakApplicationQueryService.kt @@ -8,6 +8,7 @@ import app.ehrenamtskarte.backend.common.webservice.GraphQLContext import app.ehrenamtskarte.backend.common.webservice.UnauthorizedException import com.expediagroup.graphql.generator.annotations.GraphQLDescription import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.transactions.transaction @Suppress("unused") class EakApplicationQueryService { @@ -19,11 +20,13 @@ class EakApplicationQueryService { ): List { val context = dfe.getContext() val jwtPayload = context.enforceSignedIn() - val user = AdministratorsRepository.findByIds(listOf(jwtPayload.userId))[0] - if (!Authorizer.mayViewApplicationsInRegion(user, regionId)) { - throw UnauthorizedException() - } + return transaction { + val user = AdministratorsRepository.findByIds(listOf(jwtPayload.userId))[0] + if (!Authorizer.mayViewApplicationsInRegion(user, regionId)) { + throw UnauthorizedException() + } - return ApplicationRepository.getApplications(regionId) + ApplicationRepository.getApplications(regionId) + } } }