diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 938c53c24..3932586db 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -219,18 +219,18 @@ importers: '@google-cloud/firestore': 6.5.0 '@grpc/grpc-js': 1.9.14 '@istanbuljs/nyc-config-typescript': 1.0.2_nyc@15.1.0 - '@storybook/addon-a11y': 7.6.8 - '@storybook/addon-actions': 7.6.8 + '@storybook/addon-a11y': 7.6.10 + '@storybook/addon-actions': 7.6.10 '@storybook/addon-docs': 7.0.27_react-dom@16.14.0+react@16.14.0 - '@storybook/addon-essentials': 7.6.8_8670b6b354e952731204f2707411a99b - '@storybook/addon-links': 7.6.8_react@16.14.0 - '@storybook/addon-mdx-gfm': 7.6.8 + '@storybook/addon-essentials': 7.6.10_8670b6b354e952731204f2707411a99b + '@storybook/addon-links': 7.6.10_react@16.14.0 + '@storybook/addon-mdx-gfm': 7.6.10 '@storybook/addon-postcss': 2.0.0_webpack@5.89.0 - '@storybook/addons': 7.6.8_react-dom@16.14.0+react@16.14.0 - '@storybook/cli': 7.6.8 - '@storybook/node-logger': 7.6.8 - '@storybook/react': 7.6.8_a09eeef7f0664d0f2265f09bdaae5546 - '@storybook/react-vite': 7.6.8_bd89ff102a2e2b69358623f0c313a2aa + '@storybook/addons': 7.6.10_react-dom@16.14.0+react@16.14.0 + '@storybook/cli': 7.6.10 + '@storybook/node-logger': 7.6.10 + '@storybook/react': 7.6.10_a09eeef7f0664d0f2265f09bdaae5546 + '@storybook/react-vite': 7.6.10_bd89ff102a2e2b69358623f0c313a2aa '@svgr/webpack': 5.5.0 '@testing-library/dom': 8.20.1 '@testing-library/jest-dom': 5.17.0 @@ -250,7 +250,7 @@ importers: '@typescript-eslint/eslint-plugin': 5.59.11_15db5b0353da39dbffe8aa8538bdc191 '@typescript-eslint/parser': 5.59.11_eslint@8.56.0+typescript@5.0.4 '@vitejs/plugin-react': 1.3.2 - autoprefixer: 10.4.16_postcss@8.4.33 + autoprefixer: 10.4.17_postcss@8.4.33 chromatic: 5.10.2 cypress-mochawesome-reporter: 2.4.0_mocha@8.4.0 debugging-aid: 0.5.3 @@ -277,8 +277,8 @@ importers: reselect: 4.1.8 seedrandom: 3.0.5 source-map-support: 0.5.21 - storybook: 7.6.8 - storybook-react-router: 1.0.8_d0bfa2c44302e810f9c2adc218350f06 + storybook: 7.6.10 + storybook-react-router: 1.0.8_4bd6bbd35dbdbe40496839d6d98c95c9 tailwindcss: 3.0.24_ts-node@10.9.2 tiny-warning: 1.0.3 ts-node: 10.9.2_974f63f0e1b1f88473c23d08384cf450 @@ -845,13 +845,13 @@ importers: '@eisbuk/svg': link:../svg '@eisbuk/testing': link:../testing '@eisbuk/translations': link:../translations - '@storybook/addon-actions': 7.6.8 - '@storybook/addon-essentials': 7.6.8_8670b6b354e952731204f2707411a99b - '@storybook/addon-interactions': 7.6.8 - '@storybook/addon-links': 7.6.8_react@16.14.0 - '@storybook/addon-mdx-gfm': 7.6.8 - '@storybook/react': 7.6.8_a09eeef7f0664d0f2265f09bdaae5546 - '@storybook/react-vite': 7.6.8_bd89ff102a2e2b69358623f0c313a2aa + '@storybook/addon-actions': 7.6.10 + '@storybook/addon-essentials': 7.6.10_8670b6b354e952731204f2707411a99b + '@storybook/addon-interactions': 7.6.10 + '@storybook/addon-links': 7.6.10_react@16.14.0 + '@storybook/addon-mdx-gfm': 7.6.10 + '@storybook/react': 7.6.10_a09eeef7f0664d0f2265f09bdaae5546 + '@storybook/react-vite': 7.6.10_bd89ff102a2e2b69358623f0c313a2aa '@storybook/testing-library': 0.1.0 '@testing-library/dom': 8.20.1 '@testing-library/jest-dom': 5.17.0 @@ -866,7 +866,7 @@ importers: '@typescript-eslint/parser': 5.59.11_eslint@8.56.0+typescript@5.0.4 '@vitejs/plugin-react': 1.3.2 '@vitest/ui': 0.31.4_vitest@0.31.4 - autoprefixer: 10.4.16_postcss@8.4.33 + autoprefixer: 10.4.17_postcss@8.4.33 eslint: 8.56.0 eslint-config-google: 0.14.0_eslint@8.56.0 eslint-import-resolver-typescript: 3.5.5_04f761f5210e6fcaa607caa6df343dfc @@ -881,7 +881,7 @@ importers: react: 16.14.0 react-dom: 16.14.0_react@16.14.0 react-router-dom: 5.3.4_react@16.14.0 - storybook: 7.6.8 + storybook: 7.6.10 tailwindcss: 3.0.24 typescript: 5.0.4 uuid: 8.3.2 @@ -929,7 +929,7 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.21 + '@jridgewell/trace-mapping': 0.3.22 /@apidevtools/json-schema-ref-parser/9.1.2: resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==} @@ -987,7 +987,7 @@ packages: dependencies: '@babel/types': 7.21.5 '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.21 + '@jridgewell/trace-mapping': 0.3.22 jsesc: 2.5.2 dev: true @@ -997,7 +997,7 @@ packages: dependencies: '@babel/types': 7.23.6 '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.21 + '@jridgewell/trace-mapping': 0.3.22 jsesc: 2.5.2 dev: true @@ -1088,6 +1088,21 @@ packages: - supports-color dev: true + /@babel/helper-define-polyfill-provider/0.5.0_@babel+core@7.23.7: + resolution: {integrity: sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + debug: 4.3.4 + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/helper-environment-visitor/7.22.20: resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} engines: {node: '>=6.9.0'} @@ -2080,9 +2095,9 @@ packages: '@babel/core': 7.23.7 '@babel/helper-module-imports': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - babel-plugin-polyfill-corejs2: 0.4.7_@babel+core@7.23.7 + babel-plugin-polyfill-corejs2: 0.4.8_@babel+core@7.23.7 babel-plugin-polyfill-corejs3: 0.8.7_@babel+core@7.23.7 - babel-plugin-polyfill-regenerator: 0.5.4_@babel+core@7.23.7 + babel-plugin-polyfill-regenerator: 0.5.5_@babel+core@7.23.7 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -2289,10 +2304,10 @@ packages: '@babel/plugin-transform-unicode-regex': 7.23.3_@babel+core@7.23.7 '@babel/plugin-transform-unicode-sets-regex': 7.23.3_@babel+core@7.23.7 '@babel/preset-modules': 0.1.6-no-external-plugins_@babel+core@7.23.7 - babel-plugin-polyfill-corejs2: 0.4.7_@babel+core@7.23.7 + babel-plugin-polyfill-corejs2: 0.4.8_@babel+core@7.23.7 babel-plugin-polyfill-corejs3: 0.8.7_@babel+core@7.23.7 - babel-plugin-polyfill-regenerator: 0.5.4_@babel+core@7.23.7 - core-js-compat: 3.35.0 + babel-plugin-polyfill-regenerator: 0.5.5_@babel+core@7.23.7 + core-js-compat: 3.35.1 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -4033,7 +4048,7 @@ packages: dependencies: '@babel/core': 7.23.7 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.21 + '@jridgewell/trace-mapping': 0.3.22 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -4096,7 +4111,7 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.21 + '@jridgewell/trace-mapping': 0.3.22 /@jridgewell/resolve-uri/3.1.1: resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} @@ -4110,14 +4125,14 @@ packages: resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} dependencies: '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.21 + '@jridgewell/trace-mapping': 0.3.22 dev: true /@jridgewell/sourcemap-codec/1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - /@jridgewell/trace-mapping/0.3.21: - resolution: {integrity: sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==} + /@jridgewell/trace-mapping/0.3.22: + resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 @@ -5112,17 +5127,17 @@ packages: '@sinonjs/commons': 1.8.6 dev: true - /@storybook/addon-a11y/7.6.8: - resolution: {integrity: sha512-jaBDNQzumA/0DqN1zHM3sq3QeXtMmQDKNE24D6X9nd7FLJQlN9bTHSQJC3/LM4/wt0CUlz2L8iP5Egu5VTxr1w==} + /@storybook/addon-a11y/7.6.10: + resolution: {integrity: sha512-TP17m4TAWLSSd2x9cWNg7d0MCZZCojYIG83RZMXAb55jt8gKJBMDbupOoDLydBsABQa5Uk9ZP0D/CvumMon8RA==} dependencies: - '@storybook/addon-highlight': 7.6.8 + '@storybook/addon-highlight': 7.6.10 axe-core: 4.8.3 dev: true - /@storybook/addon-actions/7.6.8: - resolution: {integrity: sha512-/KQlr/nLsAazJuSVUoMjQdwAeeXkKEtElKdqXrqI1LVOi5a7kMgB+bmn9aKX+7VBQLfQ36Btyty+FaY7bRtehQ==} + /@storybook/addon-actions/7.6.10: + resolution: {integrity: sha512-pcKmf0H/caGzKDy8cz1adNSjv+KOBWLJ11RzGExrWm+Ad5ACifwlsQPykJ3TQ/21sTd9IXVrE9uuq4LldEnPbg==} dependencies: - '@storybook/core-events': 7.6.8 + '@storybook/core-events': 7.6.10 '@storybook/global': 5.0.0 '@types/uuid': 9.0.7 dequal: 2.0.3 @@ -5130,18 +5145,18 @@ packages: uuid: 9.0.1 dev: true - /@storybook/addon-backgrounds/7.6.8: - resolution: {integrity: sha512-b+Oj41z2W/Pv6oCXmcjGdNkOStbVItrlDoIeUGyDKrngzH9Kpv5u2XZTHkZWGWusLhOVq8ENBDqj6ENRL6kDtw==} + /@storybook/addon-backgrounds/7.6.10: + resolution: {integrity: sha512-kGzsN1QkfyI8Cz7TErEx9OCB3PMzpCFGLd/iy7FreXwbMbeAQ3/9fYgKUsNOYgOhuTz7S09koZUWjS/WJuZGFA==} dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 ts-dedent: 2.2.0 dev: true - /@storybook/addon-controls/7.6.8_8670b6b354e952731204f2707411a99b: - resolution: {integrity: sha512-vjBwO1KbjB3l74qOVvLvks4LJjAIStr2n4j7Grdhqf2eeQvj122gT51dXstndtMNFqNHD4y3eImwNAbuaYrrnw==} + /@storybook/addon-controls/7.6.10_8670b6b354e952731204f2707411a99b: + resolution: {integrity: sha512-LjwCQRMWq1apLtFwDi6U8MI6ITUr+KhxJucZ60tfc58RgB2v8ayozyDAonFEONsx9YSR1dNIJ2Z/e2rWTBJeYA==} dependencies: - '@storybook/blocks': 7.6.8_8670b6b354e952731204f2707411a99b + '@storybook/blocks': 7.6.10_8670b6b354e952731204f2707411a99b lodash: 4.17.21 ts-dedent: 2.2.0 transitivePeerDependencies: @@ -5187,27 +5202,27 @@ packages: - supports-color dev: true - /@storybook/addon-docs/7.6.8_8670b6b354e952731204f2707411a99b: - resolution: {integrity: sha512-vl7jNKT8x8Hnwn38l5cUr6TQZFCmx09VxarGUrMEO4mwTOoVRL2ofoh9JKFXhCiCHlMI9R0lnupGB/LAplWgPg==} + /@storybook/addon-docs/7.6.10_8670b6b354e952731204f2707411a99b: + resolution: {integrity: sha512-GtyQ9bMx1AOOtl6ZS9vwK104HFRK+tqzxddRRxhXkpyeKu3olm9aMgXp35atE/3fJSqyyDm2vFtxxH8mzBA20A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@jest/transform': 29.7.0 '@mdx-js/react': 2.3.0_react@16.14.0 - '@storybook/blocks': 7.6.8_8670b6b354e952731204f2707411a99b - '@storybook/client-logger': 7.6.8 - '@storybook/components': 7.6.8_8670b6b354e952731204f2707411a99b - '@storybook/csf-plugin': 7.6.8 - '@storybook/csf-tools': 7.6.8 + '@storybook/blocks': 7.6.10_8670b6b354e952731204f2707411a99b + '@storybook/client-logger': 7.6.10 + '@storybook/components': 7.6.10_8670b6b354e952731204f2707411a99b + '@storybook/csf-plugin': 7.6.10 + '@storybook/csf-tools': 7.6.10 '@storybook/global': 5.0.0 '@storybook/mdx2-csf': 1.1.0 - '@storybook/node-logger': 7.6.8 - '@storybook/postinstall': 7.6.8 - '@storybook/preview-api': 7.6.8 - '@storybook/react-dom-shim': 7.6.8_react-dom@16.14.0+react@16.14.0 - '@storybook/theming': 7.6.8_react-dom@16.14.0+react@16.14.0 - '@storybook/types': 7.6.8 + '@storybook/node-logger': 7.6.10 + '@storybook/postinstall': 7.6.10 + '@storybook/preview-api': 7.6.10 + '@storybook/react-dom-shim': 7.6.10_react-dom@16.14.0+react@16.14.0 + '@storybook/theming': 7.6.10_react-dom@16.14.0+react@16.14.0 + '@storybook/types': 7.6.10 fs-extra: 11.2.0 react: 16.14.0 react-dom: 16.14.0_react@16.14.0 @@ -5221,25 +5236,25 @@ packages: - supports-color dev: true - /@storybook/addon-essentials/7.6.8_8670b6b354e952731204f2707411a99b: - resolution: {integrity: sha512-UoRZWPkDYL/UWsfAJk4q4nn5nayYdOvPApVsF/ZDnGsiv1zB2RpqbkiD1bfxPlGEVCoB+NQIN2s867gEpf+DjA==} + /@storybook/addon-essentials/7.6.10_8670b6b354e952731204f2707411a99b: + resolution: {integrity: sha512-cjbuCCK/3dtUity0Uqi5LwbkgfxqCCE5x5mXZIk9lTMeDz5vB9q6M5nzncVDy8F8przF3NbDLLgxKlt8wjiICg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/addon-actions': 7.6.8 - '@storybook/addon-backgrounds': 7.6.8 - '@storybook/addon-controls': 7.6.8_8670b6b354e952731204f2707411a99b - '@storybook/addon-docs': 7.6.8_8670b6b354e952731204f2707411a99b - '@storybook/addon-highlight': 7.6.8 - '@storybook/addon-measure': 7.6.8 - '@storybook/addon-outline': 7.6.8 - '@storybook/addon-toolbars': 7.6.8 - '@storybook/addon-viewport': 7.6.8 - '@storybook/core-common': 7.6.8 - '@storybook/manager-api': 7.6.8_react-dom@16.14.0+react@16.14.0 - '@storybook/node-logger': 7.6.8 - '@storybook/preview-api': 7.6.8 + '@storybook/addon-actions': 7.6.10 + '@storybook/addon-backgrounds': 7.6.10 + '@storybook/addon-controls': 7.6.10_8670b6b354e952731204f2707411a99b + '@storybook/addon-docs': 7.6.10_8670b6b354e952731204f2707411a99b + '@storybook/addon-highlight': 7.6.10 + '@storybook/addon-measure': 7.6.10 + '@storybook/addon-outline': 7.6.10 + '@storybook/addon-toolbars': 7.6.10 + '@storybook/addon-viewport': 7.6.10 + '@storybook/core-common': 7.6.10 + '@storybook/manager-api': 7.6.10_react-dom@16.14.0+react@16.14.0 + '@storybook/node-logger': 7.6.10 + '@storybook/preview-api': 7.6.10 react: 16.14.0 react-dom: 16.14.0_react@16.14.0 ts-dedent: 2.2.0 @@ -5250,24 +5265,24 @@ packages: - supports-color dev: true - /@storybook/addon-highlight/7.6.8: - resolution: {integrity: sha512-3mUfdLxaegCKWSm0i245RhnmEgkE+uLnOkE7h2kiztrWGqYuzGBKjgfZuVrftqsEWWc7LlJ1xdDZsIgs5Z06gA==} + /@storybook/addon-highlight/7.6.10: + resolution: {integrity: sha512-dIuS5QmoT1R+gFOcf6CoBa6D9UR5/wHCfPqPRH8dNNcCLtIGSHWQ4v964mS5OCq1Huj7CghmR15lOUk7SaYwUA==} dependencies: '@storybook/global': 5.0.0 dev: true - /@storybook/addon-interactions/7.6.8: - resolution: {integrity: sha512-E1ZMrJ/4larCPW92AFuY71I9s8Ri+DEdwNtVnU/WV55NA+E9oRKt5/qOrJLcjQorViwh9KOHeeuc8kagA2hjnA==} + /@storybook/addon-interactions/7.6.10: + resolution: {integrity: sha512-lEsAdP/PrOZK/KmRbZ/fU4RjEqDP+e/PBlVVVJT2QvHniWK/xxkjCD0axsHU/XuaeQRFhmg0/KR342PC/cIf9A==} dependencies: '@storybook/global': 5.0.0 - '@storybook/types': 7.6.8 + '@storybook/types': 7.6.10 jest-mock: 27.5.1 polished: 4.2.2 ts-dedent: 2.2.0 dev: true - /@storybook/addon-links/7.6.8_react@16.14.0: - resolution: {integrity: sha512-lw+xMvzfhyOR5I5792rGCf31OfVsiNG+uCc6CEewjKdC+e4GZDXzAkLIrLVUvbf6iUvHzERD63Y5nKz2bt5yZA==} + /@storybook/addon-links/7.6.10_react@16.14.0: + resolution: {integrity: sha512-s/WkSYHpr2pb9p57j6u/xDBg3TKJhBq55YMl0GB5gXgkRPIeuGbPhGJhm2yTGVFLvXgr/aHHnOxb/R/W8PiRhA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: @@ -5280,25 +5295,25 @@ packages: ts-dedent: 2.2.0 dev: true - /@storybook/addon-mdx-gfm/7.6.8: - resolution: {integrity: sha512-4tnn77Z9ua/kHgajjusBQ57zZFFbrzNbkNwnOeeIqiLWwGcD96FwNZrC8KpdCmfp+6VBHboeERcGwxRLCSDGEw==} + /@storybook/addon-mdx-gfm/7.6.10: + resolution: {integrity: sha512-gA1kQZJ4ZKOpi9afu7WRC1twCwZR0J1Nd7u47kNq+5coW1GH9uqGDFYHzr4mfKdD1J09/OrmfMnVjCPx9MYDtQ==} dependencies: - '@storybook/node-logger': 7.6.8 + '@storybook/node-logger': 7.6.10 remark-gfm: 3.0.1 ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color dev: true - /@storybook/addon-measure/7.6.8: - resolution: {integrity: sha512-76ItcwATq3BRPEtGV5Apby3E+7tOn6d5dtNpBYBZOdjUsj6E+uFtdmfHrc1Bt1ersJ7hRDCgsHArqOGXeLuDrw==} + /@storybook/addon-measure/7.6.10: + resolution: {integrity: sha512-OVfTI56+kc4hLWfZ/YPV3WKj/aA9e4iKXYxZyPdhfX4Z8TgZdD1wv9Z6e8DKS0H5kuybYrHKHaID5ki6t7qz3w==} dependencies: '@storybook/global': 5.0.0 tiny-invariant: 1.3.1 dev: true - /@storybook/addon-outline/7.6.8: - resolution: {integrity: sha512-eTHreyvxYLIPt5AbMyDO3CEgGClQFt+CtA/RgSjpyv9MgYXPsZp/h1ZHpYYhSPRYnRE4//YnPMuk7eLf4udaag==} + /@storybook/addon-outline/7.6.10: + resolution: {integrity: sha512-RVJrEoPArhI6zAIMNl1Gz0zrj84BTfEWYYz0yDWOTVgvN411ugsoIk1hw0671MOneXJ2RcQ9MFIeV/v6AVDQYg==} dependencies: '@storybook/global': 5.0.0 ts-dedent: 2.2.0 @@ -5317,22 +5332,22 @@ packages: - webpack dev: true - /@storybook/addon-toolbars/7.6.8: - resolution: {integrity: sha512-Akr9Pfw+AzQBRPVdo8yjcdS4IiOyEIBPVn/OAcbLi6a2zLYBdn99yKi21P0o03TJjNy32A254iAQQ7zyjIwEtA==} + /@storybook/addon-toolbars/7.6.10: + resolution: {integrity: sha512-PaXY/oj9yxF7/H0CNdQKcioincyCkfeHpISZriZbZqhyqsjn3vca7RFEmsB88Q+ou6rMeqyA9st+6e2cx/Ct6A==} dev: true - /@storybook/addon-viewport/7.6.8: - resolution: {integrity: sha512-9fvaTudqTA7HYygOWq8gnlmR5XLLjMgK4RoZqMP8OhzX0Vkkg72knPI8lyrnHwze/yMcR1e2lmbdLm55rPq6QA==} + /@storybook/addon-viewport/7.6.10: + resolution: {integrity: sha512-+bA6juC/lH4vEhk+w0rXakaG8JgLG4MOYrIudk5vJKQaC6X58LIM9N4kzIS2KSExRhkExXBPrWsnMfCo7uxmKg==} dependencies: memoizerific: 1.11.3 dev: true - /@storybook/addons/7.6.8_react-dom@16.14.0+react@16.14.0: - resolution: {integrity: sha512-M8VXkUxD+7HLKjEQT3FNk3CoOtOw4ANhxayIu5lQ4PiKwJ61YVw1r/laPyOYaIMItH/40K1yBSCSV5DDQcN/QA==} + /@storybook/addons/7.6.10_react-dom@16.14.0+react@16.14.0: + resolution: {integrity: sha512-lv/oT4ZGMKfXh6bB7LbuRP85bwRprBPYuMMl+e1Ikvu5WTfqVoJRYjc7mvXaIHGCI6DZ/nFcbRjra6q8ZhoDgw==} dependencies: - '@storybook/manager-api': 7.6.8_react-dom@16.14.0+react@16.14.0 - '@storybook/preview-api': 7.6.8 - '@storybook/types': 7.6.8 + '@storybook/manager-api': 7.6.10_react-dom@16.14.0+react@16.14.0 + '@storybook/preview-api': 7.6.10 + '@storybook/types': 7.6.10 transitivePeerDependencies: - react - react-dom @@ -5373,23 +5388,23 @@ packages: - supports-color dev: true - /@storybook/blocks/7.6.8_8670b6b354e952731204f2707411a99b: - resolution: {integrity: sha512-9cjwqj+VLmVHD8lU1xIGbZiu2xPQ3A+cAobmam045wvEB/wYhcrF0K0lBwHLqUWTcNdOzZy5uaoaCu/1G5AmDg==} + /@storybook/blocks/7.6.10_8670b6b354e952731204f2707411a99b: + resolution: {integrity: sha512-oSIukGC3yuF8pojABC/HLu5tv2axZvf60TaUs8eDg7+NiiKhzYSPoMQxs5uMrKngl+EJDB92ESgWT9vvsfvIPg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/channels': 7.6.8 - '@storybook/client-logger': 7.6.8 - '@storybook/components': 7.6.8_8670b6b354e952731204f2707411a99b - '@storybook/core-events': 7.6.8 + '@storybook/channels': 7.6.10 + '@storybook/client-logger': 7.6.10 + '@storybook/components': 7.6.10_8670b6b354e952731204f2707411a99b + '@storybook/core-events': 7.6.10 '@storybook/csf': 0.1.2 - '@storybook/docs-tools': 7.6.8 + '@storybook/docs-tools': 7.6.10 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.6.8_react-dom@16.14.0+react@16.14.0 - '@storybook/preview-api': 7.6.8 - '@storybook/theming': 7.6.8_react-dom@16.14.0+react@16.14.0 - '@storybook/types': 7.6.8 + '@storybook/manager-api': 7.6.10_react-dom@16.14.0+react@16.14.0 + '@storybook/preview-api': 7.6.10 + '@storybook/theming': 7.6.10_react-dom@16.14.0+react@16.14.0 + '@storybook/types': 7.6.10 '@types/lodash': 4.14.202 color-convert: 2.0.1 dequal: 2.0.3 @@ -5411,13 +5426,13 @@ packages: - supports-color dev: true - /@storybook/builder-manager/7.6.8: - resolution: {integrity: sha512-4CZo1RHPlDJA7G+lJoVdi+/3/L1ERxVxtvwuGgk8CxVDt6vFNpoc7fEGryNv3GRzKN1/luNYNU1MTnCUSn0B2g==} + /@storybook/builder-manager/7.6.10: + resolution: {integrity: sha512-f+YrjZwohGzvfDtH8BHzqM3xW0p4vjjg9u7uzRorqUiNIAAKHpfNrZ/WvwPlPYmrpAHt4xX/nXRJae4rFSygPw==} dependencies: '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@storybook/core-common': 7.6.8 - '@storybook/manager': 7.6.8 - '@storybook/node-logger': 7.6.8 + '@storybook/core-common': 7.6.10 + '@storybook/manager': 7.6.10 + '@storybook/node-logger': 7.6.10 '@types/ejs': 3.1.5 '@types/find-cache-dir': 3.2.1 '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15_esbuild@0.18.20 @@ -5435,8 +5450,8 @@ packages: - supports-color dev: true - /@storybook/builder-vite/7.6.8_typescript@5.0.4+vite@4.3.9: - resolution: {integrity: sha512-EC+v5n3YoTpYhe1Yk3fIa/E+jaJJAN6Udst/sWGBAc1T/f+/ECM1ee7y9PO3Zxl/wYYMFY+3hDx6OQXLAdWlcQ==} + /@storybook/builder-vite/7.6.10_typescript@5.0.4+vite@4.3.9: + resolution: {integrity: sha512-qxe19axiNJVdIKj943e1ucAmADwU42fTGgMSdBzzrvfH3pSOmx2057aIxRzd8YtBRnj327eeqpgCHYIDTunMYQ==} peerDependencies: '@preact/preset-vite': '*' typescript: '>= 4.3.x' @@ -5450,14 +5465,14 @@ packages: vite-plugin-glimmerx: optional: true dependencies: - '@storybook/channels': 7.6.8 - '@storybook/client-logger': 7.6.8 - '@storybook/core-common': 7.6.8 - '@storybook/csf-plugin': 7.6.8 - '@storybook/node-logger': 7.6.8 - '@storybook/preview': 7.6.8 - '@storybook/preview-api': 7.6.8 - '@storybook/types': 7.6.8 + '@storybook/channels': 7.6.10 + '@storybook/client-logger': 7.6.10 + '@storybook/core-common': 7.6.10 + '@storybook/csf-plugin': 7.6.10 + '@storybook/node-logger': 7.6.10 + '@storybook/preview': 7.6.10 + '@storybook/preview-api': 7.6.10 + '@storybook/types': 7.6.10 '@types/find-cache-dir': 3.2.1 browser-assert: 1.2.1 es-module-lexer: 0.9.3 @@ -5488,33 +5503,33 @@ packages: resolution: {integrity: sha512-YppvPa1qMyC+oCQJ3tf7Quzpf2NnBlvIRLPJiGAMssUwX5qE0iKe9lTtkNwMaNxEvzz6rDxewSlz+f/MWr4gPw==} dev: true - /@storybook/channels/7.6.8: - resolution: {integrity: sha512-aPgQcSjeyZDhAfr/slCphVfYGCihxuFCaCVlZuJA4uTaGEUkn+kPW2jP0yLtlSN33J79wFXsMLPQYwIS3aQ4Ew==} + /@storybook/channels/7.6.10: + resolution: {integrity: sha512-ITCLhFuDBKgxetuKnWwYqMUWlU7zsfH3gEKZltTb+9/2OAWR7ez0iqU7H6bXP1ridm0DCKkt2UMWj2mmr9iQqg==} dependencies: - '@storybook/client-logger': 7.6.8 - '@storybook/core-events': 7.6.8 + '@storybook/client-logger': 7.6.10 + '@storybook/core-events': 7.6.10 '@storybook/global': 5.0.0 qs: 6.11.2 telejson: 7.2.0 tiny-invariant: 1.3.1 dev: true - /@storybook/cli/7.6.8: - resolution: {integrity: sha512-Is8nkgsbIOu+Jk9Z7x5sgMPgGs9RTVDum3cz9eA4UspPiIBJsf7nGHAWOtc+mCIm6Z3eeNbT1YMOWxz9EuqboA==} + /@storybook/cli/7.6.10: + resolution: {integrity: sha512-pK1MEseMm73OMO2OVoSz79QWX8ymxgIGM8IeZTCo9gImiVRChMNDFYcv8yPWkjuyesY8c15CoO48aR7pdA1OjQ==} hasBin: true dependencies: '@babel/core': 7.23.7 '@babel/preset-env': 7.23.8_@babel+core@7.23.7 '@babel/types': 7.23.6 '@ndelangen/get-tarball': 3.0.9 - '@storybook/codemod': 7.6.8 - '@storybook/core-common': 7.6.8 - '@storybook/core-events': 7.6.8 - '@storybook/core-server': 7.6.8 - '@storybook/csf-tools': 7.6.8 - '@storybook/node-logger': 7.6.8 - '@storybook/telemetry': 7.6.8 - '@storybook/types': 7.6.8 + '@storybook/codemod': 7.6.10 + '@storybook/core-common': 7.6.10 + '@storybook/core-events': 7.6.10 + '@storybook/core-server': 7.6.10 + '@storybook/csf-tools': 7.6.10 + '@storybook/node-logger': 7.6.10 + '@storybook/telemetry': 7.6.10 + '@storybook/types': 7.6.10 '@types/semver': 7.5.6 '@yarnpkg/fslib': 2.10.3 '@yarnpkg/libzip': 2.3.0 @@ -5539,7 +5554,6 @@ packages: puppeteer-core: 2.1.1 read-pkg-up: 7.0.1 semver: 7.5.4 - simple-update-notifier: 2.0.0 strip-json-comments: 3.1.1 tempy: 1.0.1 ts-dedent: 2.2.0 @@ -5557,22 +5571,22 @@ packages: '@storybook/global': 5.0.0 dev: true - /@storybook/client-logger/7.6.8: - resolution: {integrity: sha512-WyK+RNSYk+sy0pxk8np1MnUXSWFdy54WqtT7u64vDFs9Jxfa1oMZ+Vl6XhaFQYR++tKC7VabLcI6vZ0pOoE9Jw==} + /@storybook/client-logger/7.6.10: + resolution: {integrity: sha512-U7bbpu21ntgePMz/mKM18qvCSWCUGCUlYru8mgVlXLCKqFqfTeP887+CsPEQf29aoE3cLgDrxqbRJ1wxX9kL9A==} dependencies: '@storybook/global': 5.0.0 dev: true - /@storybook/codemod/7.6.8: - resolution: {integrity: sha512-3Gk+ZsD35DUgqbbRNdX547kzZK/ajIbgwynmR0FuPhZhhZuYI4+2eMNzdmI/Oe9Nov4R16senQuAZjw/Dc5LrA==} + /@storybook/codemod/7.6.10: + resolution: {integrity: sha512-pzFR0nocBb94vN9QCJLC3C3dP734ZigqyPmd0ZCDj9Xce2ytfHK3v1lKB6TZWzKAZT8zztauECYxrbo4LVuagw==} dependencies: '@babel/core': 7.23.7 '@babel/preset-env': 7.23.8_@babel+core@7.23.7 '@babel/types': 7.23.6 '@storybook/csf': 0.1.2 - '@storybook/csf-tools': 7.6.8 - '@storybook/node-logger': 7.6.8 - '@storybook/types': 7.6.8 + '@storybook/csf-tools': 7.6.10 + '@storybook/node-logger': 7.6.10 + '@storybook/types': 7.6.10 '@types/cross-spawn': 6.0.6 cross-spawn: 7.0.3 globby: 11.1.0 @@ -5602,19 +5616,19 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/components/7.6.8_8670b6b354e952731204f2707411a99b: - resolution: {integrity: sha512-ghrQkws7F2s9xwdiQq2ezQoOozCiYF9g/vnh+qttd4UgKqXDWoILb8LJGKtS7C0u0vV/Ui59EYUyDIVBT6wHlw==} + /@storybook/components/7.6.10_8670b6b354e952731204f2707411a99b: + resolution: {integrity: sha512-H5hF8pxwtbt0LxV24KMMsPlbYG9Oiui3ObvAQkvGu6q62EYxRPeNSrq3GBI5XEbI33OJY9bT24cVaZx18dXqwQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@radix-ui/react-select': 1.2.2_8670b6b354e952731204f2707411a99b '@radix-ui/react-toolbar': 1.0.4_8670b6b354e952731204f2707411a99b - '@storybook/client-logger': 7.6.8 + '@storybook/client-logger': 7.6.10 '@storybook/csf': 0.1.2 '@storybook/global': 5.0.0 - '@storybook/theming': 7.6.8_react-dom@16.14.0+react@16.14.0 - '@storybook/types': 7.6.8 + '@storybook/theming': 7.6.10_react-dom@16.14.0+react@16.14.0 + '@storybook/types': 7.6.10 memoizerific: 1.11.3 react: 16.14.0 react-dom: 16.14.0_react@16.14.0 @@ -5625,11 +5639,11 @@ packages: - '@types/react-dom' dev: true - /@storybook/core-client/7.6.8: - resolution: {integrity: sha512-Avt0R0F9U+PEndPS23LHyIBxbwVCeF/VCIuIfD1eTYwE9nSLzvJXqlxARfFyhYV43LQcC5fIKjxfrsyUjM5vbQ==} + /@storybook/core-client/7.6.10: + resolution: {integrity: sha512-DjnzSzSNDmZyxyg6TxugzWQwOsW+n/iWVv6sHNEvEd5STr0mjuJjIEELmv58LIr5Lsre5+LEddqHsyuLyt8ubg==} dependencies: - '@storybook/client-logger': 7.6.8 - '@storybook/preview-api': 7.6.8 + '@storybook/client-logger': 7.6.10 + '@storybook/preview-api': 7.6.10 dev: true /@storybook/core-common/7.0.27: @@ -5637,7 +5651,7 @@ packages: dependencies: '@storybook/node-logger': 7.0.27 '@storybook/types': 7.0.27 - '@types/node': 16.18.71 + '@types/node': 16.18.74 '@types/node-fetch': 2.6.11 '@types/pretty-hrtime': 1.0.3 chalk: 4.1.2 @@ -5661,12 +5675,12 @@ packages: - supports-color dev: true - /@storybook/core-common/7.6.8: - resolution: {integrity: sha512-TRbiv5AF2m88ixyh31yqn6FgWDYZO6e6IxbJolRvEKD4b9opfPJ5e1ocb/QPz9sBUmsrX59ghMjO8R6dDYzdwA==} + /@storybook/core-common/7.6.10: + resolution: {integrity: sha512-K3YWqjCKMnpvYsWNjOciwTH6zWbuuZzmOiipziZaVJ+sB1XYmH52Y3WGEm07TZI8AYK9DRgwA13dR/7W0nw72Q==} dependencies: - '@storybook/core-events': 7.6.8 - '@storybook/node-logger': 7.6.8 - '@storybook/types': 7.6.8 + '@storybook/core-events': 7.6.10 + '@storybook/node-logger': 7.6.10 + '@storybook/types': 7.6.10 '@types/find-cache-dir': 3.2.1 '@types/node': 18.19.8 '@types/node-fetch': 2.6.11 @@ -5696,30 +5710,30 @@ packages: resolution: {integrity: sha512-sNnqgO5i5DUIqeQfNbr987KWvAciMN9FmMBuYdKjVFMqWFyr44HTgnhfKwZZKl+VMDYkHA9Do7UGSYZIKy0P4g==} dev: true - /@storybook/core-events/7.6.8: - resolution: {integrity: sha512-c1onJHG71JKbU4hMZC31rVTSbcfhcXaB0ikGnb7rJzlUZ1YkWnb0wf0/ikQR0seDOpR3HS+WQ0M3FIpqANyETg==} + /@storybook/core-events/7.6.10: + resolution: {integrity: sha512-yccDH67KoROrdZbRKwxgTswFMAco5nlCyxszCDASCLygGSV2Q2e+YuywrhchQl3U6joiWi3Ps1qWu56NeNafag==} dependencies: ts-dedent: 2.2.0 dev: true - /@storybook/core-server/7.6.8: - resolution: {integrity: sha512-/csAFNuAhF11f6D9neYNavmKPFK/ZxTskaktc4iDwBRgBM95kZ6DBFjg9ErRi5Q8Z/i92wk6qORkq4bkN/lI9w==} + /@storybook/core-server/7.6.10: + resolution: {integrity: sha512-2icnqJkn3vwq0eJPP0rNaHd7IOvxYf5q4lSVl2AWTxo/Ae19KhokI6j/2vvS2XQJMGQszwshlIwrZUNsj5p0yw==} dependencies: '@aw-web-design/x-default-browser': 1.4.126 '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 7.6.8 - '@storybook/channels': 7.6.8 - '@storybook/core-common': 7.6.8 - '@storybook/core-events': 7.6.8 + '@storybook/builder-manager': 7.6.10 + '@storybook/channels': 7.6.10 + '@storybook/core-common': 7.6.10 + '@storybook/core-events': 7.6.10 '@storybook/csf': 0.1.2 - '@storybook/csf-tools': 7.6.8 + '@storybook/csf-tools': 7.6.10 '@storybook/docs-mdx': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/manager': 7.6.8 - '@storybook/node-logger': 7.6.8 - '@storybook/preview-api': 7.6.8 - '@storybook/telemetry': 7.6.8 - '@storybook/types': 7.6.8 + '@storybook/manager': 7.6.10 + '@storybook/node-logger': 7.6.10 + '@storybook/preview-api': 7.6.10 + '@storybook/telemetry': 7.6.10 + '@storybook/types': 7.6.10 '@types/detect-port': 1.3.5 '@types/node': 18.19.8 '@types/pretty-hrtime': 1.0.3 @@ -5762,10 +5776,10 @@ packages: - supports-color dev: true - /@storybook/csf-plugin/7.6.8: - resolution: {integrity: sha512-KYh7VwTHhXz/V9weuGY3pK9messE56TJHUD+0SO9dF2BVNKsKpAOVcjzrE6masiAFX35Dz/t9ywy8iFcfAo0dg==} + /@storybook/csf-plugin/7.6.10: + resolution: {integrity: sha512-Sc+zZg/BnPH2X28tthNaQBnDiFfO0QmfjVoOx0fGYM9SvY3P5ehzWwp5hMRBim6a/twOTzePADtqYL+t6GMqqg==} dependencies: - '@storybook/csf-tools': 7.6.8 + '@storybook/csf-tools': 7.6.10 unplugin: 1.6.0 transitivePeerDependencies: - supports-color @@ -5787,15 +5801,15 @@ packages: - supports-color dev: true - /@storybook/csf-tools/7.6.8: - resolution: {integrity: sha512-ea6QnQRvhPOpSUbfioLlJYRLpJldNZcocgUJwOJ/e3TM6M67BZBzeDnVOJkuUKejrp++KF22GEIkbGAWErIlnA==} + /@storybook/csf-tools/7.6.10: + resolution: {integrity: sha512-TnDNAwIALcN6SA4l00Cb67G02XMOrYU38bIpFJk5VMDX2dvgPjUtJNBuLmEbybGcOt7nPyyFIHzKcY5FCVGoWA==} dependencies: '@babel/generator': 7.23.6 '@babel/parser': 7.23.6 '@babel/traverse': 7.23.7 '@babel/types': 7.23.6 '@storybook/csf': 0.1.2 - '@storybook/types': 7.6.8 + '@storybook/types': 7.6.10 fs-extra: 11.2.0 recast: 0.23.4 ts-dedent: 2.2.0 @@ -5828,12 +5842,12 @@ packages: - supports-color dev: true - /@storybook/docs-tools/7.6.8: - resolution: {integrity: sha512-zIbrje4JLFpfK05y3SkDNtIth/vTOEaJVa/zaHuwS1gUX73Pq3jwF2eMGVabeVWi6hvxGeZXhnIsymh/Hpbn5w==} + /@storybook/docs-tools/7.6.10: + resolution: {integrity: sha512-UgbikducoXzqQHf2TozO0f2rshaeBNnShVbL5Ai4oW7pDymBmrfzdjGbF/milO7yxNKcoIByeoNmu384eBamgQ==} dependencies: - '@storybook/core-common': 7.6.8 - '@storybook/preview-api': 7.6.8 - '@storybook/types': 7.6.8 + '@storybook/core-common': 7.6.10 + '@storybook/preview-api': 7.6.10 + '@storybook/types': 7.6.10 '@types/doctrine': 0.0.3 assert: 2.1.0 doctrine: 3.0.0 @@ -5847,14 +5861,14 @@ packages: resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} dev: true - /@storybook/instrumenter/7.6.8: - resolution: {integrity: sha512-AZZoMmQJ753uIeNJpjV+qUSREMu1gfucqslo858uPxpWwhWmr6pI9a5V19w5sedGXT/I9WiTt8hQVmkg4lsWbA==} + /@storybook/instrumenter/7.6.10: + resolution: {integrity: sha512-9FYXW1CKXnZ7yYmy2A6U0seqJMe1F7g55J28Vslk3ZLoGATFJ2BR0eoQS+cgfBly6djehjaVeuV3IcUYGnQ/6Q==} dependencies: - '@storybook/channels': 7.6.8 - '@storybook/client-logger': 7.6.8 - '@storybook/core-events': 7.6.8 + '@storybook/channels': 7.6.10 + '@storybook/client-logger': 7.6.10 + '@storybook/core-events': 7.6.10 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.6.8 + '@storybook/preview-api': 7.6.10 '@vitest/utils': 0.34.7 util: 0.12.5 dev: true @@ -5884,17 +5898,17 @@ packages: ts-dedent: 2.2.0 dev: true - /@storybook/manager-api/7.6.8_react-dom@16.14.0+react@16.14.0: - resolution: {integrity: sha512-BGVZb0wMTd8Hi8rUYPRzdIhWRw73qXlEupwEYyGtH63sg+aD67wyAo8/pMEpQBH4kVss7VheWY2JGpRJeFVUxw==} + /@storybook/manager-api/7.6.10_react-dom@16.14.0+react@16.14.0: + resolution: {integrity: sha512-8eGVpRlpunuFScDtc7nxpPJf/4kJBAAZlNdlhmX09j8M3voX6GpcxabBamSEX5pXZqhwxQCshD4IbqBmjvadlw==} dependencies: - '@storybook/channels': 7.6.8 - '@storybook/client-logger': 7.6.8 - '@storybook/core-events': 7.6.8 + '@storybook/channels': 7.6.10 + '@storybook/client-logger': 7.6.10 + '@storybook/core-events': 7.6.10 '@storybook/csf': 0.1.2 '@storybook/global': 5.0.0 - '@storybook/router': 7.6.8 - '@storybook/theming': 7.6.8_react-dom@16.14.0+react@16.14.0 - '@storybook/types': 7.6.8 + '@storybook/router': 7.6.10 + '@storybook/theming': 7.6.10_react-dom@16.14.0+react@16.14.0 + '@storybook/types': 7.6.10 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 @@ -5906,8 +5920,8 @@ packages: - react-dom dev: true - /@storybook/manager/7.6.8: - resolution: {integrity: sha512-INoXXoHXyw9PPMJAOAhwf9u2GNDDNdv1JAI1fhrbCAECzDabHT9lRVUo6v8I5XMc+YdMHLM1Vz38DbB+w18hFw==} + /@storybook/manager/7.6.10: + resolution: {integrity: sha512-Co3sLCbNYY6O4iH2ggmRDLCPWLj03JE5s/DOG8OVoXc6vBwTc/Qgiyrsxxp6BHQnPpM0mxL6aKAxE3UjsW/Nog==} dev: true /@storybook/mdx2-csf/1.1.0: @@ -5919,7 +5933,7 @@ packages: dependencies: '@types/npmlog': 4.1.6 chalk: 4.1.2 - core-js: 3.35.0 + core-js: 3.35.1 npmlog: 5.0.1 pretty-hrtime: 1.0.3 dev: true @@ -5933,16 +5947,16 @@ packages: pretty-hrtime: 1.0.3 dev: true - /@storybook/node-logger/7.6.8: - resolution: {integrity: sha512-SVvwZAcOLdkstqnAbE5hVYsriXh6OXjLcwFEBpAYi1meQ0R70iNALVSPEfIDK1r7M163Jngsq2hRnHvbLoQNkg==} + /@storybook/node-logger/7.6.10: + resolution: {integrity: sha512-ZBuqrv4bjJzKXyfRGFkVIi+z6ekn6rOPoQao4KmsfLNQAUUsEdR8Baw/zMnnU417zw5dSEaZdpuwx75SCQAeOA==} dev: true /@storybook/postinstall/7.0.27: resolution: {integrity: sha512-VehWuUQxTlqSfTEl3rnufA9+aBbFIv802c8HMJ6SsnwRSb93vlc2ZDGxx3hzryQhbBuI8oNDQx0VdFVwn+MkEg==} dev: true - /@storybook/postinstall/7.6.8: - resolution: {integrity: sha512-9ixyNpoT1w3WmSooCzndAWDnw4fENA1WUBcdqrzlcgaSBKiAHad1k/Yct/uBAU95l/uQ13NgXK3mx4+S6unx/g==} + /@storybook/postinstall/7.6.10: + resolution: {integrity: sha512-SMdXtednPCy3+SRJ7oN1OPN1oVFhj3ih+ChOEX8/kZ5J3nfmV3wLPtsZvFGUCf0KWQEP1xL+1Urv48mzMKcV/w==} dev: true /@storybook/preview-api/7.0.27: @@ -5965,15 +5979,15 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/preview-api/7.6.8: - resolution: {integrity: sha512-rtP9Yo8ZV1NWhtA3xCOAb1vU70KCV3D2U4E3rOb2prqJ2CEQ/MQbrB7KUTDRSQdT7VFbjsLQWVCTUcNo29U8JQ==} + /@storybook/preview-api/7.6.10: + resolution: {integrity: sha512-5A3etoIwZCx05yuv3KSTv1wynN4SR4rrzaIs/CTBp3BC4q1RBL+Or/tClk0IJPXQMlx/4Y134GtNIBbkiDofpw==} dependencies: - '@storybook/channels': 7.6.8 - '@storybook/client-logger': 7.6.8 - '@storybook/core-events': 7.6.8 + '@storybook/channels': 7.6.10 + '@storybook/client-logger': 7.6.10 + '@storybook/core-events': 7.6.10 '@storybook/csf': 0.1.2 '@storybook/global': 5.0.0 - '@storybook/types': 7.6.8 + '@storybook/types': 7.6.10 '@types/qs': 6.9.11 dequal: 2.0.3 lodash: 4.17.21 @@ -5984,8 +5998,8 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/preview/7.6.8: - resolution: {integrity: sha512-f54EXmJcIkc5A7nQmtnCUtNFNfEOoTuPYFK7pDfcK/bVU+g63zzWhBAeIUZ8yioLKGqZPTzFEhXkpa+OqsT0Jg==} + /@storybook/preview/7.6.10: + resolution: {integrity: sha512-F07BzVXTD3byq+KTWtvsw3pUu3fQbyiBNLFr2CnfU4XSdLKja5lDt8VqDQq70TayVQOf5qfUTzRd4M6pQkjw1w==} dev: true /@storybook/react-dom-shim/7.0.27_react-dom@16.14.0+react@16.14.0: @@ -5998,8 +6012,8 @@ packages: react-dom: 16.14.0_react@16.14.0 dev: true - /@storybook/react-dom-shim/7.6.8_react-dom@16.14.0+react@16.14.0: - resolution: {integrity: sha512-NIvtjdXCTwd0VA/zCaCuCYv7L35nze7qDsFW6JhSHyqB7fKyIEMSbluktO2VISotHOSkgZ2zA+rGpk3O8yh6lg==} + /@storybook/react-dom-shim/7.6.10_react-dom@16.14.0+react@16.14.0: + resolution: {integrity: sha512-M+N/h6ximacaFdIDjMN2waNoWwApeVYTpFeoDppiFTvdBTXChyIuiPgYX9QSg7gDz92OaA52myGOot4wGvXVzg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6008,8 +6022,8 @@ packages: react-dom: 16.14.0_react@16.14.0 dev: true - /@storybook/react-vite/7.6.8_bd89ff102a2e2b69358623f0c313a2aa: - resolution: {integrity: sha512-Hlr07oaEdI7LpWV3dCLpDhrzaKuzoPwEYOrxV8iyiu77DfWMoz8w8T2Jfl9OI45WgaqW0n81vXgaoyu//zL2SA==} + /@storybook/react-vite/7.6.10_bd89ff102a2e2b69358623f0c313a2aa: + resolution: {integrity: sha512-YE2+J1wy8nO+c6Nv/hBMu91Edew3K184L1KSnfoZV8vtq2074k1Me/8pfe0QNuq631AncpfCYNb37yBAXQ/80w==} engines: {node: '>=16'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6018,8 +6032,8 @@ packages: dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0_typescript@5.0.4+vite@4.3.9 '@rollup/pluginutils': 5.1.0 - '@storybook/builder-vite': 7.6.8_typescript@5.0.4+vite@4.3.9 - '@storybook/react': 7.6.8_a09eeef7f0664d0f2265f09bdaae5546 + '@storybook/builder-vite': 7.6.10_typescript@5.0.4+vite@4.3.9 + '@storybook/react': 7.6.10_a09eeef7f0664d0f2265f09bdaae5546 '@vitejs/plugin-react': 3.1.0_vite@4.3.9 magic-string: 0.30.5 react: 16.14.0 @@ -6035,8 +6049,8 @@ packages: - vite-plugin-glimmerx dev: true - /@storybook/react/7.6.8_a09eeef7f0664d0f2265f09bdaae5546: - resolution: {integrity: sha512-yMqcCNskCxqoYSGWO1qu6Jdju9zhEEwd8tOC7AgIC8sAB7K8FTxZu0d6+QFpeg9fGq+hyAmRM4GrT9Fq9IKwwQ==} + /@storybook/react/7.6.10_a09eeef7f0664d0f2265f09bdaae5546: + resolution: {integrity: sha512-wwBn1cg2uZWW4peqqBjjU7XGmFq8HdkVUtWwh6dpfgmlY1Aopi+vPgZt7pY9KkWcTOq5+DerMdSfwxukpc3ajQ==} engines: {node: '>=16.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6046,13 +6060,13 @@ packages: typescript: optional: true dependencies: - '@storybook/client-logger': 7.6.8 - '@storybook/core-client': 7.6.8 - '@storybook/docs-tools': 7.6.8 + '@storybook/client-logger': 7.6.10 + '@storybook/core-client': 7.6.10 + '@storybook/docs-tools': 7.6.10 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.6.8 - '@storybook/react-dom-shim': 7.6.8_react-dom@16.14.0+react@16.14.0 - '@storybook/types': 7.6.8 + '@storybook/preview-api': 7.6.10 + '@storybook/react-dom-shim': 7.6.10_react-dom@16.14.0+react@16.14.0 + '@storybook/types': 7.6.10 '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 '@types/node': 18.19.8 @@ -6088,20 +6102,20 @@ packages: react-dom: 16.14.0_react@16.14.0 dev: true - /@storybook/router/7.6.8: - resolution: {integrity: sha512-pFoq22w1kEwduqMpGX3FPSSukdWLMX6UQa2Cw4MDW+hzp3vhC7+3MVaBG5ShQAjGv46NNcSgsIUkyarlU5wd/A==} + /@storybook/router/7.6.10: + resolution: {integrity: sha512-G/H4Jn2+y8PDe8Zbq4DVxF/TPn0/goSItdILts39JENucHiuGBCjKjSWGBe1rkwKi1tUbB3yhxJVrLagxFEPpQ==} dependencies: - '@storybook/client-logger': 7.6.8 + '@storybook/client-logger': 7.6.10 memoizerific: 1.11.3 qs: 6.11.2 dev: true - /@storybook/telemetry/7.6.8: - resolution: {integrity: sha512-hHUS3fyHjKR3ZdbG+/OVI+pwXXKOmS8L8GMuWKlpUovvCYBLm0/Q0MUQ9XaLuByOCzvAurqB3Owp3ZV7GiY30Q==} + /@storybook/telemetry/7.6.10: + resolution: {integrity: sha512-p3mOSUtIyy2tF1z6pQXxNh1JzYFcAm97nUgkwLzF07GfEdVAPM+ftRSLFbD93zVvLEkmLTlsTiiKaDvOY/lQWg==} dependencies: - '@storybook/client-logger': 7.6.8 - '@storybook/core-common': 7.6.8 - '@storybook/csf-tools': 7.6.8 + '@storybook/client-logger': 7.6.10 + '@storybook/core-common': 7.6.10 + '@storybook/csf-tools': 7.6.10 chalk: 4.1.2 detect-package-manager: 2.0.1 fetch-retry: 5.0.6 @@ -6115,8 +6129,8 @@ packages: /@storybook/testing-library/0.1.0: resolution: {integrity: sha512-g947f4LJZw3IluBhysMKLJXByAFiSxnGuooENqU+ZPt/GTrz1I9GDBlhmoTJahuFkVbwHvziAl/8riY2Re921g==} dependencies: - '@storybook/client-logger': 7.6.8 - '@storybook/instrumenter': 7.6.8 + '@storybook/client-logger': 7.6.10 + '@storybook/instrumenter': 7.6.10 '@testing-library/dom': 8.20.1 '@testing-library/user-event': 13.5.0_@testing-library+dom@8.20.1 ts-dedent: 2.2.0 @@ -6136,14 +6150,14 @@ packages: react-dom: 16.14.0_react@16.14.0 dev: true - /@storybook/theming/7.6.8_react-dom@16.14.0+react@16.14.0: - resolution: {integrity: sha512-0ervBgeYGieifjISlFS7x5QZF9vNgLtHHlYKdkrAsACTK+VfB0JglVwFdLrgzAKxQRlVompaxl3TecFGWlvhtw==} + /@storybook/theming/7.6.10_react-dom@16.14.0+react@16.14.0: + resolution: {integrity: sha512-f5tuy7yV3TOP3fIboSqpgLHy0wKayAw/M8HxX0jVET4Z4fWlFK0BiHJabQ+XEdAfQM97XhPFHB2IPbwsqhCEcQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@emotion/use-insertion-effect-with-fallbacks': 1.0.1_react@16.14.0 - '@storybook/client-logger': 7.6.8 + '@storybook/client-logger': 7.6.10 '@storybook/global': 5.0.0 memoizerific: 1.11.3 react: 16.14.0 @@ -6159,10 +6173,10 @@ packages: file-system-cache: 2.3.0 dev: true - /@storybook/types/7.6.8: - resolution: {integrity: sha512-+mABX20OhwJjqULocG5Betfidwrlk+Kq+grti+LAYwYsdBwxctBNSrqK8P9r8XDFL6PbppZeExGiHKwGu6WsKQ==} + /@storybook/types/7.6.10: + resolution: {integrity: sha512-hcS2HloJblaMpCAj2axgGV+53kgSRYPT0a1PG1IHsZaYQILfHSMmBqM8XzXXYTsgf9250kz3dqFX1l0n3EqMlQ==} dependencies: - '@storybook/channels': 7.6.8 + '@storybook/channels': 7.6.10 '@types/babel__core': 7.20.5 '@types/express': 4.17.21 file-system-cache: 2.3.0 @@ -6792,8 +6806,8 @@ packages: form-data: 4.0.0 dev: true - /@types/node/16.18.71: - resolution: {integrity: sha512-ARO+458bNJQeNEFuPyT6W+q9ULotmsQzhV3XABsFSxEvRMUYENcBsNAHWYPlahU+UHa5gCVwyKT1Z3f1Wwr26Q==} + /@types/node/16.18.74: + resolution: {integrity: sha512-eEn8RkzZFcT0gb8qyi0CcfSOQnLE+NbGLIIaxGGmjn/N35v/C3M8ohxcpSlNlCv+H8vPpMGmrGDdCkzr8xu2tQ==} dev: true /@types/node/18.19.8: @@ -7982,15 +7996,15 @@ packages: engines: {node: '>= 4.0.0'} dev: true - /autoprefixer/10.4.16_postcss@8.4.33: - resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==} + /autoprefixer/10.4.17_postcss@8.4.33: + resolution: {integrity: sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 dependencies: browserslist: 4.22.2 - caniuse-lite: 1.0.30001578 + caniuse-lite: 1.0.30001579 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -8081,14 +8095,14 @@ packages: resolve: 1.22.8 dev: false - /babel-plugin-polyfill-corejs2/0.4.7_@babel+core@7.23.7: - resolution: {integrity: sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==} + /babel-plugin-polyfill-corejs2/0.4.8_@babel+core@7.23.7: + resolution: {integrity: sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: '@babel/compat-data': 7.23.5 '@babel/core': 7.23.7 - '@babel/helper-define-polyfill-provider': 0.4.4_@babel+core@7.23.7 + '@babel/helper-define-polyfill-provider': 0.5.0_@babel+core@7.23.7 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -8101,18 +8115,18 @@ packages: dependencies: '@babel/core': 7.23.7 '@babel/helper-define-polyfill-provider': 0.4.4_@babel+core@7.23.7 - core-js-compat: 3.35.0 + core-js-compat: 3.35.1 transitivePeerDependencies: - supports-color dev: true - /babel-plugin-polyfill-regenerator/0.5.4_@babel+core@7.23.7: - resolution: {integrity: sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==} + /babel-plugin-polyfill-regenerator/0.5.5_@babel+core@7.23.7: + resolution: {integrity: sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: '@babel/core': 7.23.7 - '@babel/helper-define-polyfill-provider': 0.4.4_@babel+core@7.23.7 + '@babel/helper-define-polyfill-provider': 0.5.0_@babel+core@7.23.7 transitivePeerDependencies: - supports-color dev: true @@ -8326,8 +8340,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001578 - electron-to-chromium: 1.4.634 + caniuse-lite: 1.0.30001579 + electron-to-chromium: 1.4.640 node-releases: 2.0.14 update-browserslist-db: 1.0.13_browserslist@4.22.2 dev: true @@ -8462,8 +8476,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - /caniuse-lite/1.0.30001578: - resolution: {integrity: sha512-J/jkFgsQ3NEl4w2lCoM9ZPxrD+FoBNJ7uJUpGVjIg/j0OwJosWM36EPDv+Yyi0V4twBk9pPmlFS+PLykgEvUmg==} + /caniuse-lite/1.0.30001579: + resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==} dev: true /cardinal/2.1.1: @@ -9016,14 +9030,14 @@ packages: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} - /core-js-compat/3.35.0: - resolution: {integrity: sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==} + /core-js-compat/3.35.1: + resolution: {integrity: sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==} dependencies: browserslist: 4.22.2 dev: true - /core-js/3.35.0: - resolution: {integrity: sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==} + /core-js/3.35.1: + resolution: {integrity: sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==} requiresBuild: true dev: true @@ -9764,8 +9778,8 @@ packages: engines: {node: '>=12'} dev: true - /dotenv/16.3.1: - resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + /dotenv/16.3.2: + resolution: {integrity: sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==} engines: {node: '>=12'} dev: true @@ -9780,7 +9794,7 @@ packages: end-of-stream: 1.4.4 inherits: 2.0.4 readable-stream: 2.3.8 - stream-shift: 1.0.2 + stream-shift: 1.0.3 dev: true /duplexify/4.1.2: @@ -9789,7 +9803,7 @@ packages: end-of-stream: 1.4.4 inherits: 2.0.4 readable-stream: 3.6.2 - stream-shift: 1.0.2 + stream-shift: 1.0.3 /eastasianwidth/0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -9816,8 +9830,8 @@ packages: jake: 10.8.7 dev: true - /electron-to-chromium/1.4.634: - resolution: {integrity: sha512-gQNahJfF5AE4MZo+pMSwmnwkzVZ+F4ZGGj4Z/MMddOXVQM0y9OHy6ts3W9SDzAJaiZM3p6eixn5ABCQ+AfXzcQ==} + /electron-to-chromium/1.4.640: + resolution: {integrity: sha512-z/6oZ/Muqk4BaE7P69bXhUhpJbUM9ZJeka43ZwxsDshKtePns4mhBlh8bU5+yrnOnz3fhG82XLzGUXazOmsWnA==} dev: true /elegant-spinner/1.0.1: @@ -11277,8 +11291,8 @@ packages: /flatted/3.2.9: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} - /flow-parser/0.226.0: - resolution: {integrity: sha512-YlH+Y/P/5s0S7Vg14RwXlJMF/JsGfkG7gcKB/zljyoqaPNX9YVsGzx+g6MLTbhZaWbPhs4347aTpmSb9GgiPtw==} + /flow-parser/0.227.0: + resolution: {integrity: sha512-nOygtGKcX/siZK/lFzpfdHEfOkfGcTW7rNroR1Zsz6T/JxSahPALXVt5qVHq/fgvMJuv096BTKbgxN3PzVBaDA==} engines: {node: '>=0.4.0'} dev: true @@ -13513,7 +13527,7 @@ packages: '@babel/register': 7.23.7_@babel+core@7.23.7 babel-core: 7.0.0-bridge.0_@babel+core@7.23.7 chalk: 4.1.2 - flow-parser: 0.226.0 + flow-parser: 0.227.0 graceful-fs: 4.2.11 micromatch: 4.0.5 neo-async: 2.6.2 @@ -13839,7 +13853,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: app-root-dir: 1.0.2 - dotenv: 16.3.1 + dotenv: 16.3.2 dotenv-expand: 10.0.0 dev: true @@ -13991,7 +14005,7 @@ packages: enquirer: 2.4.1 log-update: 4.0.0 p-map: 4.0.0 - rfdc: 1.3.0 + rfdc: 1.3.1 rxjs: 7.8.1 through: 2.3.8 wrap-ansi: 7.0.0 @@ -17405,8 +17419,8 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - /rfdc/1.3.0: - resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + /rfdc/1.3.1: + resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} dev: true /rimraf/2.6.3: @@ -17701,13 +17715,6 @@ packages: dependencies: is-arrayish: 0.3.2 - /simple-update-notifier/2.0.0: - resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} - engines: {node: '>=10'} - dependencies: - semver: 7.5.4 - dev: true - /sirv/2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -17930,7 +17937,7 @@ packages: resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} dev: true - /storybook-react-router/1.0.8_d0bfa2c44302e810f9c2adc218350f06: + /storybook-react-router/1.0.8_4bd6bbd35dbdbe40496839d6d98c95c9: resolution: {integrity: sha512-3PvuTu6cJHtF72WC3aKdIrUY1eUYsdVYEmW74nZiJoLCn0C/6iDx73w94jgI6lXq75aEJsri+lEDyhcokfP6YA==} peerDependencies: '@storybook/addon-actions': ^4.0.0||^5.0.0||5.2.0-beta.13 @@ -17938,18 +17945,18 @@ packages: react-dom: '*' react-router: ^4.0.0||^5.0.0 dependencies: - '@storybook/addon-actions': 7.6.8 + '@storybook/addon-actions': 7.6.10 prop-types: 15.8.1 react: 16.14.0 react-dom: 16.14.0_react@16.14.0 react-router: 5.3.4_react@16.14.0 dev: true - /storybook/7.6.8: - resolution: {integrity: sha512-ugRtDSs2eTgHMOZ3wKXbUEbPnlJ2XImPbnvxNssK14py2mHKwPnhSqLNrjlQMkmkO13GdjalLDyj4lZtoYdo0Q==} + /storybook/7.6.10: + resolution: {integrity: sha512-ypFeGhQTUBBfqSUVZYh7wS5ghn3O2wILCiQc4459SeUpvUn+skcqw/TlrwGSoF5EWjDA7gtRrWDxO3mnlPt5Cw==} hasBin: true dependencies: - '@storybook/cli': 7.6.8 + '@storybook/cli': 7.6.10 transitivePeerDependencies: - bufferutil - encoding @@ -17971,8 +17978,8 @@ packages: dependencies: stream-chain: 2.2.5 - /stream-shift/1.0.2: - resolution: {integrity: sha512-rV4Bovi9xx0BFzOb/X0B2GqoIjvqPCttZdu0Wgtx2Dxkj7ETyWl9gmqJ4EutWRLvtZWm8dxE+InQZX1IryZn/w==} + /stream-shift/1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} /string-argv/0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} @@ -18495,16 +18502,16 @@ packages: uglify-js: optional: true dependencies: - '@jridgewell/trace-mapping': 0.3.21 + '@jridgewell/trace-mapping': 0.3.22 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 - terser: 5.26.0 + terser: 5.27.0 webpack: 5.89.0 dev: true - /terser/5.26.0: - resolution: {integrity: sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==} + /terser/5.27.0: + resolution: {integrity: sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==} engines: {node: '>=10'} hasBin: true dependencies: @@ -19330,7 +19337,7 @@ packages: resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} engines: {node: '>=10.12.0'} dependencies: - '@jridgewell/trace-mapping': 0.3.21 + '@jridgewell/trace-mapping': 0.3.22 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 dev: false diff --git a/common/config/rush/repo-state.json b/common/config/rush/repo-state.json index 373c9e13f..ff67e0854 100644 --- a/common/config/rush/repo-state.json +++ b/common/config/rush/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "c8781aa0eecdca84acfe762ce15c915e5bf08828", + "pnpmShrinkwrapHash": "fad0f80cd56291c19d54901d4bb76389a9449c1a", "preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f" } diff --git a/packages/client/src/AppContent.tsx b/packages/client/src/AppContent.tsx index b0e30142d..df02fde37 100644 --- a/packages/client/src/AppContent.tsx +++ b/packages/client/src/AppContent.tsx @@ -25,6 +25,7 @@ import ErrorBoundaryPage from "@/pages/error_boundary"; import SlotsPage from "@/pages/slots"; import LoginPage from "@/pages/login"; import CustomerAreaPage from "@/pages/customer_area"; +import SelectAccount from "@/pages/select_account"; import AttendancePrintable from "@/pages/attendance_printable"; import DebugPage from "@/pages/debug"; import AdminPreferencesPage from "@/pages/admin_preferences"; @@ -111,6 +112,7 @@ const AppContent: React.FC = () => { > + { const { t } = useTranslation(); - const { email } = useSelector(getBookingsCustomer) || {}; + const { email } = useSelector(getBookingsCustomer(secretKey)) || {}; const hasBookingsForCalendar = useSelector(getHasBookingsForCalendar); const { open: openModal } = useSendICSModal({ secretKey, email }); diff --git a/packages/client/src/components/auth/LoginRoute.tsx b/packages/client/src/components/auth/LoginRoute.tsx index 903a94e18..d317f6bb4 100644 --- a/packages/client/src/components/auth/LoginRoute.tsx +++ b/packages/client/src/components/auth/LoginRoute.tsx @@ -2,14 +2,9 @@ import React from "react"; import { Route, Redirect, RouteProps } from "react-router-dom"; import { useSelector } from "react-redux"; -import { PrivateRoutes, Routes } from "@eisbuk/shared/ui"; +import { PrivateRoutes } from "@eisbuk/shared/ui"; -import { - getAllSecretKeys, - getIsAdmin, - getIsAuthEmpty, - getIsAuthLoaded, -} from "@/store/selectors/auth"; +import { getIsAuthEmpty, getIsAuthLoaded } from "@/store/selectors/auth"; import Loading from "./Loading"; /** @@ -21,8 +16,6 @@ import Loading from "./Loading"; const LoginRoute: React.FC = (props) => { const isAuthEmpty = useSelector(getIsAuthEmpty); const isAuthLoaded = useSelector(getIsAuthLoaded); - const isAdmin = useSelector(getIsAdmin); - const [secretKey] = useSelector(getAllSecretKeys) || []; switch (true) { // Loading screen @@ -31,16 +24,10 @@ const LoginRoute: React.FC = (props) => { // If auth empty, show login/register screen case isAuthEmpty: return ; - // If admin, redirect to root page (attendance view) - case isAdmin: - return ; - // If not admin, but has 'secretKey' redirect to customer area - case Boolean(secretKey): - return ; - // Default: auth user exists, but is not admin nor is registered (doesn't have a 'secretKey'): - // Redirect to self registration form + // If auth exists, redirect to the default route + // Any further redirect will be handled by the PrivateRoute component there default: - return ; + return ; } }; diff --git a/packages/client/src/components/auth/PrivateRoute.tsx b/packages/client/src/components/auth/PrivateRoute.tsx index f4935ed4f..e28251de2 100644 --- a/packages/client/src/components/auth/PrivateRoute.tsx +++ b/packages/client/src/components/auth/PrivateRoute.tsx @@ -22,7 +22,7 @@ const PrivateRoute: React.FC = (props) => { const isAuthEmpty = useSelector(getIsAuthEmpty); const isAdmin = useSelector(getIsAdmin); const isAuthLoaded = useSelector(getIsAuthLoaded); - const [secretKey] = useSelector(getAllSecretKeys) || []; + const secretKeys = useSelector(getAllSecretKeys) || []; switch (true) { // Display loading state until initial auth is loaded @@ -37,9 +37,16 @@ const PrivateRoute: React.FC = (props) => { case isAuthEmpty: return ; - case Boolean(secretKey): + // If there's only one secret key, (user is managing only one account) redirect to that account + case secretKeys.length === 1: + const [secretKey] = secretKeys as string[]; return ; + // If there are multiple secret keys (user is managing multiple accounts), redirect to account selection page + // Note: This is the private route - this should not affect the behaviour of routes like '/customer_area/:secretKey' or '/self_register' + case secretKeys.length > 1: + return ; + // The auth exists - user exists in firebase auth, but there's no secret // key - redirect to complte registration default: diff --git a/packages/client/src/controllers/AthleteAvatar.tsx b/packages/client/src/controllers/AthleteAvatar.tsx index cbc2e9ea1..b7555299d 100644 --- a/packages/client/src/controllers/AthleteAvatar.tsx +++ b/packages/client/src/controllers/AthleteAvatar.tsx @@ -20,6 +20,10 @@ const AthletAvatatController: React.FC = ({ content={ + history.push(`${Routes.CustomerArea}/${secretKey}`) + } + onAddAccount={() => history.push(Routes.SelfRegister)} onLogout={async () => { await signOut(getAuth()); history.push(Routes.Login); diff --git a/packages/client/src/controllers/BookingsCountdown/BookingsCountdownContainer.tsx b/packages/client/src/controllers/BookingsCountdown/BookingsCountdownContainer.tsx index f49a1c631..26891e209 100644 --- a/packages/client/src/controllers/BookingsCountdown/BookingsCountdownContainer.tsx +++ b/packages/client/src/controllers/BookingsCountdown/BookingsCountdownContainer.tsx @@ -23,11 +23,11 @@ interface Props extends React.HTMLAttributes { * state to the store (for bookings finalisation). */ const BookingsCountdownContainer: React.FC = (props) => { - const currentDate = useSelector(getCalendarDay); - const countdownProps = useSelector(getCountdownProps); - const isMonthEmpty = useSelector(getMonthEmptyForBooking); - const { id: customerId } = useSelector(getBookingsCustomer) || {}; const secretKey = useSelector(getSecretKey)!; + const currentDate = useSelector(getCalendarDay); + const countdownProps = useSelector(getCountdownProps(secretKey)); + const isMonthEmpty = useSelector(getMonthEmptyForBooking(secretKey)); + const { id: customerId } = useSelector(getBookingsCustomer(secretKey)) || {}; const { openWithProps: openFinalizeBookingsDialog } = useFinalizeBooksingsModal(); diff --git a/packages/client/src/controllers/BookingsCountdown/__tests__/BookingsCountdownContainer.test.tsx b/packages/client/src/controllers/BookingsCountdown/__tests__/BookingsCountdownContainer.test.tsx index cbca8e667..8e4835f6f 100644 --- a/packages/client/src/controllers/BookingsCountdown/__tests__/BookingsCountdownContainer.test.tsx +++ b/packages/client/src/controllers/BookingsCountdown/__tests__/BookingsCountdownContainer.test.tsx @@ -14,7 +14,7 @@ import { updateLocalDocuments } from "@eisbuk/react-redux-firebase-firestore"; import BookingsCountdownContainer from "../BookingsCountdownContainer"; import { getNewStore } from "@/store/createStore"; -import { changeCalendarDate } from "@/store/actions/appActions"; +import { changeCalendarDate, storeSecretKey } from "@/store/actions/appActions"; import { renderWithRedux } from "@/__testUtils__/wrappers"; @@ -60,13 +60,14 @@ describe("BookingsCountdown", () => { }) ); store.dispatch(changeCalendarDate(month)); + store.dispatch(storeSecretKey(saul.secretKey)); // With test state set up, 'finalize' button should be in the screen for // provided 'month' renderWithRedux(, store); screen.getByText(i18n.t(ActionButton.FinalizeBookings) as string).click(); const wantModal = { component: "FinalizeBookingsDialog", - props: { customerId: saul.id, month }, + props: expect.objectContaining({ customerId: saul.id, month }), }; const mockDispatchCallPayload = mockDispatch.mock.calls[0][0].payload; expect(mockDispatchCallPayload.component).toEqual(wantModal.component); diff --git a/packages/client/src/controllers/PrivacyPolicyToast/PrivacyPolicyToast.tsx b/packages/client/src/controllers/PrivacyPolicyToast/PrivacyPolicyToast.tsx index 696bf1f00..42f4b8a52 100644 --- a/packages/client/src/controllers/PrivacyPolicyToast/PrivacyPolicyToast.tsx +++ b/packages/client/src/controllers/PrivacyPolicyToast/PrivacyPolicyToast.tsx @@ -17,8 +17,8 @@ const PrivacyPolicyToast: React.FC = () => { const policyParams = useSelector(getPrivacyPolicy); - const bookingsCustomer = useSelector(getBookingsCustomer) || {}; const secretKey = useSelector(getSecretKey) || ""; + const bookingsCustomer = useSelector(getBookingsCustomer(secretKey)) || {}; const policyAccepted = Boolean(bookingsCustomer.privacyPolicyAccepted); const isAdmin = useSelector(getIsAdmin); diff --git a/packages/client/src/pages/customer_area/index.tsx b/packages/client/src/pages/customer_area/index.tsx index f8f0241a9..df49a1bc4 100644 --- a/packages/client/src/pages/customer_area/index.tsx +++ b/packages/client/src/pages/customer_area/index.tsx @@ -11,7 +11,10 @@ import { OrgSubCollection, } from "@eisbuk/shared"; import { Routes } from "@eisbuk/shared/ui"; -import { useFirestoreSubscribe } from "@eisbuk/react-redux-firebase-firestore"; +import { + useFirestoreSubscribe, + useUpdateSubscription, +} from "@eisbuk/react-redux-firebase-firestore"; import { getOrganization } from "@/lib/getters"; @@ -22,14 +25,17 @@ import ProfileView from "./views/Profile"; import { useDate } from "./hooks"; import useSecretKey from "@/hooks/useSecretKey"; +import Layout from "@/controllers/Layout"; import PrivacyPolicyToast from "@/controllers/PrivacyPolicyToast"; import AthleteAvatar from "@/controllers/AthleteAvatar"; import ErrorBoundary from "@/components/atoms/ErrorBoundary"; -import Layout from "@/controllers/Layout"; - -import { getBookingsCustomer } from "@/store/selectors/bookings"; +import { + getBookingsCustomer, + getOtherBookingsAccounts, +} from "@/store/selectors/bookings"; +import { getAllSecretKeys } from "@/store/selectors/auth"; enum Views { Book = "BookView", @@ -50,21 +56,48 @@ const viewsLookup = { const CustomerArea: React.FC = () => { const secretKey = useSecretKey(); + // We're providing a fallback [secretKey] as we have multiple ways of authenticating. If authenticating + // using firebase auth, the user will have all of their secret keys in the store (this is the preferred way). + // However, user can simply use a booking link (which includes the secret key). For this method, the user doesn't have + // to authenticate with firebase auth, no secret keys will be found in auth section of the store and 'getAllSecretKeys' selector + // will return 'undefined' + const secretKeysInStore = useSelector(getAllSecretKeys); + const secretKeys = secretKeysInStore?.length + ? secretKeysInStore + : [secretKey]; + // Subscribe to necessary collections useFirestoreSubscribe(getOrganization(), [ { collection: OrgSubCollection.SlotsByDay }, { collection: OrgSubCollection.SlotBookingsCounts }, { collection: Collection.PublicOrgInfo }, - { collection: OrgSubCollection.Bookings, meta: { secretKey } }, + { + collection: OrgSubCollection.Bookings, + meta: { secretKeys }, + }, { collection: BookingSubCollection.BookedSlots, meta: { secretKey } }, { collection: BookingSubCollection.AttendedSlots, meta: { secretKey } }, { collection: BookingSubCollection.Calendar, meta: { secretKey } }, ]); + useUpdateSubscription( + { collection: OrgSubCollection.Bookings, meta: { secretKeys } }, + [secretKeys] + ); + useUpdateSubscription( + { collection: BookingSubCollection.BookedSlots, meta: { secretKey } }, + [secretKey] + ); + useUpdateSubscription( + { collection: BookingSubCollection.AttendedSlots, meta: { secretKey } }, + [secretKey] + ); + const calendarNavProps = useDate(); // Get customer data necessary for rendering/functionality - const userData = useSelector(getBookingsCustomer) || {}; + const currentAthlete = useSelector(getBookingsCustomer(secretKey)) || {}; + const otherAccounts = useSelector(getOtherBookingsAccounts(secretKey)); const [view, setView] = useState(Views.Book); const CustomerView = viewsLookup[view]; @@ -95,14 +128,19 @@ const CustomerArea: React.FC = () => { ); - if (secretKey && userData.deleted) { + if (secretKey && currentAthlete.deleted) { return ; } return ( } + userAvatar={ + + } > {view !== "ProfileView" && ( { const { t } = useTranslation(); - const customer = useSelector(getBookingsCustomer); + const secretKey = useSelector(getSecretKey) || ""; + const customer = useSelector(getBookingsCustomer(secretKey)); const orgEmail = useSelector(getOrgEmail); - const daysToRender = useSelector(getSlotsForBooking); + const daysToRender = useSelector(getSlotsForBooking(secretKey)); const date = useSelector(getCalendarDay); - const disabled = !useSelector(getIsBookingAllowed(date)); + const disabled = !useSelector(getIsBookingAllowed(secretKey, date)); const { handleBooking, handleCancellation } = useBooking(); diff --git a/packages/client/src/pages/customer_area/views/Calendar.tsx b/packages/client/src/pages/customer_area/views/Calendar.tsx index c8cfdbf52..8974d0377 100644 --- a/packages/client/src/pages/customer_area/views/Calendar.tsx +++ b/packages/client/src/pages/customer_area/views/Calendar.tsx @@ -28,7 +28,7 @@ const CalendarView: React.FC = () => { const currentDate = useSelector(getCalendarDay); const secretKey = useSelector(getSecretKey)!; - const disabled = !useSelector(getIsBookingAllowed(currentDate)); + const disabled = !useSelector(getIsBookingAllowed(secretKey, currentDate)); const bookedAndAttendedSlots = useSelector( getBookedAndAttendedSlotsForCalendar diff --git a/packages/client/src/pages/customer_area/views/Profile.tsx b/packages/client/src/pages/customer_area/views/Profile.tsx index ab7e4a9f3..7793a04d0 100644 --- a/packages/client/src/pages/customer_area/views/Profile.tsx +++ b/packages/client/src/pages/customer_area/views/Profile.tsx @@ -16,7 +16,8 @@ const CalendarView: React.FC = () => { const secretKey = useSelector(getSecretKey)!; const defaultCountryCode = useSelector(getDefaultCountryCode); - const customer = useSelector(getBookingsCustomer) || ({} as Customer); + const customer = + useSelector(getBookingsCustomer(secretKey)) || ({} as Customer); return ( = ({ backgroundIndex }: Props) => { const secretKey = useSecretKey(); useFirestoreSubscribe(getOrganization(), [ - { collection: OrgSubCollection.Bookings, meta: { secretKey } }, + { + collection: OrgSubCollection.Bookings, + meta: { secretKeys: [secretKey] }, + }, ]); - const customer = useSelector(getBookingsCustomer); + const customer = useSelector(getBookingsCustomer(secretKey)); // If customer's not deleted - you shouldn't be here // If there's no customer - the customer is either not yet loaded, or not found - both are valid reasons to stick around diff --git a/packages/client/src/pages/select_account/index.tsx b/packages/client/src/pages/select_account/index.tsx new file mode 100644 index 000000000..cfdafbf09 --- /dev/null +++ b/packages/client/src/pages/select_account/index.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { useSelector } from "react-redux"; +import { Link } from "react-router-dom"; + +import { + useFirestoreSubscribe, + useUpdateSubscription, +} from "@eisbuk/react-redux-firebase-firestore"; +import { OrgSubCollection } from "@eisbuk/shared"; +import { Routes } from "@eisbuk/shared/ui"; +import { LayoutContent } from "@eisbuk/ui"; + +import Layout from "@/controllers/Layout"; + +import ErrorBoundary from "@/components/atoms/ErrorBoundary"; + +import { getOrganization } from "@/lib/getters"; + +import { getAllSecretKeys } from "@/store/selectors/auth"; +import { getOtherBookingsAccounts } from "@/store/selectors/bookings"; + +const SelectAccount: React.FC = () => { + const secretKeys = useSelector(getAllSecretKeys) || []; + const accounts = useSelector(getOtherBookingsAccounts("")); + + // Subscribe to necessary collections + useFirestoreSubscribe(getOrganization(), [ + { + collection: OrgSubCollection.Bookings, + meta: { secretKeys }, + }, + ]); + + useUpdateSubscription( + { collection: OrgSubCollection.Bookings, meta: { secretKeys } }, + [secretKeys] + ); + + return ( + + + +
+

+ Select account +

+
+ {accounts?.map((account) => ( + + {account.name} +
+ + {account.surname} + + + ))} +
+
+
+
+
+ ); +}; + +export default SelectAccount; diff --git a/packages/client/src/pages/self_register/index.tsx b/packages/client/src/pages/self_register/index.tsx index fce35bd8e..f3a05d15e 100644 --- a/packages/client/src/pages/self_register/index.tsx +++ b/packages/client/src/pages/self_register/index.tsx @@ -24,9 +24,10 @@ import { getAuthPhoneNumber, getIsAuthEmpty, getIsAuthLoaded, + getLocalAuth, } from "@/store/selectors/auth"; -import { signOut } from "@/store/actions/authOperations"; +import { signOut, updateAuthUser } from "@/store/actions/authOperations"; import { customerSelfRegister } from "@/store/actions/bookingOperations"; import { getDefaultCountryCode, @@ -76,6 +77,11 @@ const SelfRegisterPage: React.FC = () => { } if (secretKey) { + await updateAuthUser(getLocalAuth(getState()))(dispatch, getState, { + getFirestore: () => + FirestoreVariant.client({ instance: getFirestore() }), + getFunctions: () => functions, + } as any); history.push([Routes.CustomerArea, secretKey].join("/")); } }; diff --git a/packages/client/src/store/actions/icsCalendarOperations.ts b/packages/client/src/store/actions/icsCalendarOperations.ts index 8aec7f179..db489d2ee 100644 --- a/packages/client/src/store/actions/icsCalendarOperations.ts +++ b/packages/client/src/store/actions/icsCalendarOperations.ts @@ -46,7 +46,7 @@ export const sendBookingsCalendar = const previousCalendar = getCalendarEventsByMonth(monthStr)(getState()); - const { name, surname } = getBookingsCustomer(getState()); + const { name, surname } = getBookingsCustomer(secretKey)(getState()); const { displayName = __organization__, location = "" } = getAboutOrganization(getState())[__organization__] || {}; diff --git a/packages/client/src/store/selectors/bookings/__tests__/bookings.test.ts b/packages/client/src/store/selectors/bookings/__tests__/bookings.test.ts index 5f9023213..e43f11b9c 100644 --- a/packages/client/src/store/selectors/bookings/__tests__/bookings.test.ts +++ b/packages/client/src/store/selectors/bookings/__tests__/bookings.test.ts @@ -88,7 +88,7 @@ describe("Selectors ->", () => { slotsByDay, date, }); - const res = getSlotsForCustomer(store.getState()); + const res = getSlotsForCustomer(saul.secretKey)(store.getState()); expect(res).toEqual(expectedMonthCustomer); }); @@ -129,7 +129,7 @@ describe("Selectors ->", () => { slotBookingsCounts, date, }); - const res = getSlotsForCustomer(store.getState()); + const res = getSlotsForCustomer(saul.secretKey)(store.getState()); // Slot 1 should be returned, slot 2 should be filtered out (fully booked) expect(res).toEqual({ "2021-01-01": { "slot-1": slot1 } }); }); @@ -180,7 +180,7 @@ describe("Selectors ->", () => { bookedSlots, date, }); - const res = getSlotsForCustomer(store.getState()); + const res = getSlotsForCustomer(saul.secretKey)(store.getState()); // Since slot 2 is booked by the customer, it should be returned (even if at full capacity) expect(res).toEqual(slotsByDay["2021-01"]); }); @@ -192,9 +192,9 @@ describe("Selectors ->", () => { // set up test with `isAdmin === true` const store = getNewStore(); store.dispatch({ type: Action.UpdateAdminStatus, payload: true }); - expect(getIsBookingAllowed(DateTime.now())(store.getState())).toEqual( - true - ); + expect( + getIsBookingAllowed(saul.secretKey, DateTime.now())(store.getState()) + ).toEqual(true); }); test("should allow booking if the booking deadline hasn't passed", () => { @@ -209,7 +209,9 @@ describe("Selectors ->", () => { // create local mock date to be one day before the deadline const localMockDate = currentMonthDeadline.minus({ days: 1 }); dateNowSpy.mockReturnValueOnce(localMockDate.toMillis()); - expect(getIsBookingAllowed(currentDate)(store.getState())).toEqual(true); + expect( + getIsBookingAllowed(saul.secretKey, currentDate)(store.getState()) + ).toEqual(true); }); test("should not allow booking if the booking deadline for the month is passed", () => { @@ -224,7 +226,9 @@ describe("Selectors ->", () => { // create local mock date to be one day after the deadline const localMockDate = currentMonthDeadline.plus({ days: 1 }); dateNowSpy.mockReturnValueOnce(localMockDate.toMillis()); - expect(getIsBookingAllowed(currentDate)(store.getState())).toEqual(false); + expect( + getIsBookingAllowed(saul.secretKey, currentDate)(store.getState()) + ).toEqual(false); }); test("should allow booking if within extended date period", () => { @@ -239,7 +243,9 @@ describe("Selectors ->", () => { [saul.secretKey]: sanitizeCustomer({ ...saul, extendedDate }), }) ); - expect(getIsBookingAllowed(currentDate)(store.getState())).toEqual(true); + expect( + getIsBookingAllowed(saul.secretKey, currentDate)(store.getState()) + ).toEqual(true); }); test("should not allow booking if in extended date period, but extended date has passed", () => { @@ -254,7 +260,9 @@ describe("Selectors ->", () => { [saul.secretKey]: sanitizeCustomer({ ...saul, extendedDate }), }) ); - expect(getIsBookingAllowed(currentDate)(store.getState())).toEqual(false); + expect( + getIsBookingAllowed(saul.secretKey, currentDate)(store.getState()) + ).toEqual(false); }); test("edge case: should not allow booking if extended date exists for future month, but current month deadline has already passed", () => { @@ -269,7 +277,9 @@ describe("Selectors ->", () => { [saul.secretKey]: sanitizeCustomer({ ...saul, extendedDate }), }) ); - expect(getIsBookingAllowed(currentDate)(store.getState())).toEqual(false); + expect( + getIsBookingAllowed(saul.secretKey, currentDate)(store.getState()) + ).toEqual(false); }); }); @@ -292,7 +302,9 @@ describe("Selectors ->", () => { .minus({ days: 5 }) .endOf("day"), }; - expect(getCountdownProps(store.getState())).toEqual(expectedRes); + expect(getCountdownProps(saul.secretKey)(store.getState())).toEqual( + expectedRes + ); }); test("should display countdown for second deadline if extended date belongs to observed month", () => { @@ -315,7 +327,9 @@ describe("Selectors ->", () => { month: currentDate.startOf("month"), deadline: extendedDateLuxon, }; - expect(getCountdownProps(store.getState())).toEqual(expectedRes); + expect(getCountdownProps(saul.secretKey)(store.getState())).toEqual( + expectedRes + ); }); test("should not display any countdown for admin", () => { @@ -323,7 +337,9 @@ describe("Selectors ->", () => { const currentDate = mockDate.plus({ months: 2 }); store.dispatch(changeCalendarDate(currentDate)); store.dispatch({ type: Action.UpdateAdminStatus, payload: true }); - expect(getCountdownProps(store.getState())).toEqual(undefined); + expect(getCountdownProps(saul.secretKey)(store.getState())).toEqual( + undefined + ); }); test("should display bookings are locked message (instead of countdown) if bookings for this period are locked", () => { @@ -336,7 +352,9 @@ describe("Selectors ->", () => { deadline: null, month: currentDate.startOf("month"), }; - expect(getCountdownProps(store.getState())).toEqual(expectedRes); + expect(getCountdownProps(saul.secretKey)(store.getState())).toEqual( + expectedRes + ); }); }); @@ -358,7 +376,9 @@ describe("Selectors ->", () => { date, slotsByDay: { [currentMonthString]: slotsByDay }, }); - expect(getMonthEmptyForBooking(store.getState())).toEqual(wantRes); + expect( + getMonthEmptyForBooking(saul.secretKey)(store.getState()) + ).toEqual(wantRes); }) ); diff --git a/packages/client/src/store/selectors/bookings/countdown.ts b/packages/client/src/store/selectors/bookings/countdown.ts index 8e5101d14..441c372ca 100644 --- a/packages/client/src/store/selectors/bookings/countdown.ts +++ b/packages/client/src/store/selectors/bookings/countdown.ts @@ -16,14 +16,14 @@ import { getMonthDiff } from "@/utils/date"; * @returns {boolean} `true` if admin or period for booking of the slots hasn't passed */ export const getIsBookingAllowed = - (currentDate: DateTime) => + (secretKey: string, currentDate: DateTime) => (state: LocalStore): boolean => { // admins should always be able to update bookings slots if (getIsAdmin(state)) return true; const deadline = getMonthDeadline(currentDate); - const extendedDate = getExtendedDate(state); + const extendedDate = getExtendedDate(secretKey)(state); const isExtendedDateAplicable = Boolean( extendedDate && extendedDate.diffNow().milliseconds > 0 && @@ -39,47 +39,47 @@ export const getIsBookingAllowed = * @returns `undefined` if admin (or should be hidden), otherwise returns an object * containing countdown `message`, booking `month`, and countdown `deadline` */ -export const getCountdownProps = ( - state: LocalStore -): CountdownProps | undefined => { - // return early if admin (no countdown is shown) - const isAdmin = getIsAdmin(state); - if (isAdmin) { - return undefined; - } - - const currentDate = getCalendarDay(state); - - const month = currentDate.startOf("month"); - - if (!getIsBookingAllowed(currentDate)(state)) { - return { - month, - deadline: null, - variant: BookingsCountdownVariant.BookingsLocked, - }; - } - - const monthsDeadline = getMonthDeadline(currentDate); - const extendedDate = getExtendedDate(state); - - const isExtendedDateApplicable = - extendedDate && getMonthDiff(extendedDate, currentDate) === 0; - - if (isExtendedDateApplicable) { - return { - month, - deadline: extendedDate.endOf("day"), - variant: BookingsCountdownVariant.SecondDeadline, - }; - } else { - return { - month, - deadline: monthsDeadline, - variant: BookingsCountdownVariant.FirstDeadline, - }; - } -}; +export const getCountdownProps = + (secretKey: string) => + (state: LocalStore): CountdownProps | undefined => { + // return early if admin (no countdown is shown) + const isAdmin = getIsAdmin(state); + if (isAdmin) { + return undefined; + } + + const currentDate = getCalendarDay(state); + + const month = currentDate.startOf("month"); + + if (!getIsBookingAllowed(secretKey, currentDate)(state)) { + return { + month, + deadline: null, + variant: BookingsCountdownVariant.BookingsLocked, + }; + } + + const monthsDeadline = getMonthDeadline(currentDate); + const extendedDate = getExtendedDate(secretKey)(state); + + const isExtendedDateApplicable = + extendedDate && getMonthDiff(extendedDate, currentDate) === 0; + + if (isExtendedDateApplicable) { + return { + month, + deadline: extendedDate.endOf("day"), + variant: BookingsCountdownVariant.SecondDeadline, + }; + } else { + return { + month, + deadline: monthsDeadline, + variant: BookingsCountdownVariant.FirstDeadline, + }; + } + }; // #region temp /** @TEMP should be read from admin preferences in the store */ @@ -94,14 +94,16 @@ const lockingPeriod = 5; * @param state * @returns */ -const getExtendedDate = (state: LocalStore): DateTime | undefined => { - const { extendedDate } = getBookingsCustomer(state) || {}; +const getExtendedDate = + (secretKey: string) => + (state: LocalStore): DateTime | undefined => { + const { extendedDate } = getBookingsCustomer(secretKey)(state) || {}; - // check if extended date exists - if (!extendedDate) return undefined; + // check if extended date exists + if (!extendedDate) return undefined; - return DateTime.fromISO(extendedDate).endOf("day"); -}; + return DateTime.fromISO(extendedDate).endOf("day"); + }; /** * Returns deadline for a current month calculated by subtracting diff --git a/packages/client/src/store/selectors/bookings/customer.ts b/packages/client/src/store/selectors/bookings/customer.ts index 4ca8ce833..11934dfee 100644 --- a/packages/client/src/store/selectors/bookings/customer.ts +++ b/packages/client/src/store/selectors/bookings/customer.ts @@ -3,21 +3,28 @@ import { Customer } from "@eisbuk/shared"; import { LocalStore } from "@/types/store"; /** - * Get customer info for bookings from local store - * @param state Local Redux Store - * @returns customer data (Bookings meta) + * Get bookings customer (for the provided secretKey) from store. + * Since we support multiple accounts for single auth, there might be mutilple bookings + * customers in store, hence this HOF (accepting secretKey) and returning a selector. */ -export const getBookingsCustomer = (state: LocalStore): Customer => { - // get extended date (if any) - const bookingsInStore = Object.values(state.firestore?.data.bookings || {}); - if (bookingsInStore.length > 1) { - /** @TODO */ - // this shouldn't happen in production and we're working on a way to fix it completely - // this is just a reporting feature in case it happens - console.error( - "There seem to be multiple entries in 'firestore.data.bookings' part of the local store" - ); - } +export const getBookingsCustomer = + (secretKey: string) => + (state: LocalStore): Customer => + (state.firestore?.data.bookings || {})[secretKey]; + +/** + * Returns all bookings customers currently present in store. + */ +export const getAllBookingsAccounts = (state: LocalStore): Customer[] => + Object.values(state.firestore.data.bookings || {}); - return bookingsInStore[0]; -}; +/** + * Returns all bookings customers currently present in store, except for the "current" one (matched by passed secret key). + * @param currentSecretKey + */ +export const getOtherBookingsAccounts = + (currentSecretKey: string) => + (state: LocalStore): Customer[] => + getAllBookingsAccounts(state).filter( + ({ secretKey }) => secretKey !== currentSecretKey + ); diff --git a/packages/client/src/store/selectors/bookings/slots.ts b/packages/client/src/store/selectors/bookings/slots.ts index 6b8d6a075..64f535b26 100644 --- a/packages/client/src/store/selectors/bookings/slots.ts +++ b/packages/client/src/store/selectors/bookings/slots.ts @@ -40,98 +40,108 @@ type SlotsForBooking = { slots: (SlotInterface & { interval?: string })[]; }[]; -export const getSlotsForBooking = (state: LocalStore): SlotsForBooking => { - const slotsMonth = getSlotsForCustomer(state); - const bookedSlots = getBookedSlots(state); - const customerData = getBookingsCustomer(state); - - // Sort dates so that the final output is sorted - const daysToRender = Object.keys(slotsMonth).sort((a, b) => (a < b ? -1 : 1)); - if (!daysToRender.length || !customerData) { - return []; - } - - return daysToRender.map((date) => ({ - date, - slots: Object.values(slotsMonth[date]) - .sort(({ intervals: i1 }, { intervals: i2 }) => { - const ts1 = getSlotTimespan(i1).replace(" ", ""); - const ts2 = getSlotTimespan(i2).replace(" ", ""); - - return comparePeriodsEarliestFirst(ts1, ts2); - }) - .map((slot) => - // If slot booked add interval to the return structure - bookedSlots[slot.id] - ? { ...slot, interval: bookedSlots[slot.id].interval } - : slot - ), - })); -}; +export const getSlotsForBooking = + (secretKey: string) => + (state: LocalStore): SlotsForBooking => { + const slotsMonth = getSlotsForCustomer(secretKey)(state); + const bookedSlots = getBookedSlots(state); + const customerData = getBookingsCustomer(secretKey)(state); + + // Sort dates so that the final output is sorted + const daysToRender = Object.keys(slotsMonth).sort((a, b) => + a < b ? -1 : 1 + ); + if (!daysToRender.length || !customerData) { + return []; + } + + return daysToRender.map((date) => ({ + date, + slots: Object.values(slotsMonth[date]) + .sort(({ intervals: i1 }, { intervals: i2 }) => { + const ts1 = getSlotTimespan(i1).replace(" ", ""); + const ts2 = getSlotTimespan(i2).replace(" ", ""); + + return comparePeriodsEarliestFirst(ts1, ts2); + }) + .map((slot) => + // If slot booked add interval to the return structure + bookedSlots[slot.id] + ? { ...slot, interval: bookedSlots[slot.id].interval } + : slot + ), + })); + }; /** * Get `slotsByDay` entry, from store, for current month filtered according to customer's category. * Both the `category` and `date` are read directly from store. Slots booked at full capacity are filtered out. */ -export const getSlotsForCustomer = (state: LocalStore): SlotsByDay => { - const date = getCalendarDay(state); - const categories = getBookingsCustomer(state)?.categories; - - // Return early if no category found in store - if (!categories) { - return {}; - } - - const allSlotsInStore = state.firestore.data?.slotsByDay; - const bookingsCounts = state.firestore.data?.slotBookingsCounts || {}; - - // Return early if no slots in store - if (!allSlotsInStore) return {}; - - // Get slots for current month - const monthString = date.startOf("month").toISO().substring(0, 7); - const slotsForAMonth = allSlotsInStore[monthString] || {}; - const bookingCountsForAMonth = bookingsCounts[monthString] || {}; - const bookedSlots = getBookedSlots(state); - - // Filter slots from each day with respect to category - // - // Start: Iterable of [date, SlotsById (record of slots keyed by slotId)] tuples - const processedSlots = wrapIter(Object.entries(slotsForAMonth)) - // Map: [date, SlotsById] -> [date, [slotId, SlotInterface][]] - .map(valueMapper((slots) => Object.entries(slots))) - // FlatMap: [date, [slotId, SlotInterface][]] -> [date, slotId, SlotInterface] - .flatMap(([date, slots]) => - slots.map(([id, slot]) => [date, id, slot] as const) - ) - // Filter out slots not matching customer's category - .filter(([, , slot]) => categories.some((c) => slot.categories.includes(c))) - // Filter out slots booked at full capacity (or without any capacity set) - .filter( - ([, slotId, slot]) => - // If booking count is not available, this is a no-op: - // - this makes it safe for tests (no need for additional setup) - // - with current production requirements, this is right - we filter the slots only if the capacity is set - // and the booking count is availabe (if not, the slot is not yet booked) - !bookingCountsForAMonth[slotId] || - !slot.capacity || - slot.capacity > bookingCountsForAMonth[slotId] || - Boolean(bookedSlots[slotId]) - ) - // GroupEntries by date: [date, [slotId, SlotInterface][]] - ._group( - ([date, id, slot]) => - [date, [id, slot]] as [string, [string, SlotInterface]] - ) - // Map the value: [date, [slotId, SlotInterface][]] -> [date, SlotsById] - .map(valueMapper((slots) => Object.fromEntries(slots))); - - return Object.fromEntries(processedSlots); -}; - -export const getMonthEmptyForBooking = (state: LocalStore): boolean => { - return isEmpty(getSlotsForCustomer(state)); -}; +export const getSlotsForCustomer = + (secretKey: string) => + (state: LocalStore): SlotsByDay => { + const date = getCalendarDay(state); + const categories = getBookingsCustomer(secretKey)(state)?.categories; + + // Return early if no category found in store + if (!categories) { + return {}; + } + + const allSlotsInStore = state.firestore.data?.slotsByDay; + const bookingsCounts = state.firestore.data?.slotBookingsCounts || {}; + + // Return early if no slots in store + if (!allSlotsInStore) return {}; + + // Get slots for current month + const monthString = date.startOf("month").toISO().substring(0, 7); + const slotsForAMonth = allSlotsInStore[monthString] || {}; + const bookingCountsForAMonth = bookingsCounts[monthString] || {}; + const bookedSlots = getBookedSlots(state); + + // Filter slots from each day with respect to category + // + // Start: Iterable of [date, SlotsById (record of slots keyed by slotId)] tuples + const processedSlots = wrapIter(Object.entries(slotsForAMonth)) + // Map: [date, SlotsById] -> [date, [slotId, SlotInterface][]] + .map(valueMapper((slots) => Object.entries(slots))) + // FlatMap: [date, [slotId, SlotInterface][]] -> [date, slotId, SlotInterface] + .flatMap(([date, slots]) => + slots.map(([id, slot]) => [date, id, slot] as const) + ) + // Filter out slots not matching customer's category + .filter(([, , slot]) => + categories.some((c) => slot.categories.includes(c)) + ) + // Filter out slots booked at full capacity (or without any capacity set) + .filter( + ([, slotId, slot]) => + // If booking count is not available, this is a no-op: + // - this makes it safe for tests (no need for additional setup) + // - with current production requirements, this is right - we filter the slots only if the capacity is set + // and the booking count is availabe (if not, the slot is not yet booked) + !bookingCountsForAMonth[slotId] || + !slot.capacity || + slot.capacity > bookingCountsForAMonth[slotId] || + Boolean(bookedSlots[slotId]) + ) + // GroupEntries by date: [date, [slotId, SlotInterface][]] + ._group( + ([date, id, slot]) => + [date, [id, slot]] as [string, [string, SlotInterface]] + ) + // Map the value: [date, [slotId, SlotInterface][]] -> [date, SlotsById] + .map(valueMapper((slots) => Object.fromEntries(slots))); + + return Object.fromEntries(processedSlots); + }; + +export const getMonthEmptyForBooking = + (secretKey: string) => + (state: LocalStore): boolean => { + return isEmpty(getSlotsForCustomer(secretKey)(state)); + }; type BookingsEntry = SlotInterface & { interval: SlotInterval; diff --git a/packages/e2e/__testData__/customers.json b/packages/e2e/__testData__/customers.json index 7c6bd8756..6413a583e 100644 --- a/packages/e2e/__testData__/customers.json +++ b/packages/e2e/__testData__/customers.json @@ -9,7 +9,7 @@ "phone": "+393456777", "birthday": "2001-03-01", "categories": ["competitive"], - "secretKey": "123445", + "secretKey": "000001", "subscriptionNumber": "", "password": "sauls_pass_123" }, @@ -23,7 +23,7 @@ "email": "walt@im_the_one_who.knocks", "phone": "+123456777", "birthday": "2002-03-01", - "secretKey": "000001", + "secretKey": "000002", "subscriptionNumber": "" }, @@ -36,7 +36,7 @@ "email": "gus@lospollos.me", "phone": "+123456777", "birthday": "2001-03-02", - "secretKey": "000002", + "secretKey": "000003", "subscriptionNumber": "" }, @@ -49,10 +49,20 @@ "email": "mike@lospollos.me", "phone": "+123456777", "birthday": "2021-01-01", - "secretKey": "000002", + "secretKey": "000004", "subscriptionNumber": "" }, + "morticia": { + "id": "morticia", + "name": "Morticia", + "surname": "Addams", + "certificateExpiration": "1983-09-31", + "categories": ["competitive"], + "email": "morticia@addamsfamily.com", + "password": "wednesdayschildisfullofwoe", + "secretKey": "000005" + }, "wednesday": { "id": "wednesday", "name": "Wednesday", @@ -60,17 +70,37 @@ "certificateExpiration": "2006-10-13", "categories": ["course-minors"], "email": "morticia@addamsfamily.com", - "secretKey": "000013" + "secretKey": "000006" }, - "morticia": { - "id": "morticia", - "name": "Morticia", + "pugsley": { + "id": "pugsley", + "name": "Pugsley", "surname": "Addams", - "certificateExpiration": "1983-09-31", - "categories": ["competitive"], + "certificateExpiration": "2006-10-13", + "categories": ["course-minors"], "email": "morticia@addamsfamily.com", - "password": "wednesdayschildisfullofwoe", - "secretKey": "101010" + "secretKey": "000007" + }, + + "erlich": { + "id": "erlich", + "name": "Erlich", + "surname": "Bachman", + "certificateExpiration": "2006-10-13", + "categories": ["course-minors"], + "email": "erlich@aviato.com", + "phone": "+391111111", + "secretKey": "000008" + }, + "yang": { + "id": "yang", + "name": "Yang", + "surname": "Jian", + "certificateExpiration": "2006-10-13", + "categories": ["course-minors"], + "email": "mike.hunt@isyourrefrigeratorrunning.com", + "phone": "+391111111", + "secretKey": "000009" } } } diff --git a/packages/e2e/__testData__/saul_with_extended_date.json b/packages/e2e/__testData__/saul_with_extended_date.json index 5362b601b..3bcf575aa 100644 --- a/packages/e2e/__testData__/saul_with_extended_date.json +++ b/packages/e2e/__testData__/saul_with_extended_date.json @@ -9,7 +9,7 @@ "phone": "+123456777", "birthday": "2001-01-01", "categories": ["competitive"], - "secretKey": "123445", + "secretKey": "000001", "subscriptionNumber": "41", "extendedDate": "2022-01-05" } diff --git a/packages/e2e/__testData__/slots.json b/packages/e2e/__testData__/slots.json deleted file mode 100644 index 141753377..000000000 --- a/packages/e2e/__testData__/slots.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "slots": { - "slot-1": { - "date": "2022-01-01", - "id": "slot-1", - "type": "ice", - "categories": ["competitive", "course-adults"], - "intervals": { - "09:00-10:00": { - "startTime": "09:00", - "endTime": "10:00" - }, - "09:00-11:00": { - "startTime": "09:00", - "endTime": "11:00" - }, - "10:00-11:00": { - "startTime": "10:00", - "endTime": "11:00" - } - }, - "notes": "" - }, - "slot-2": { - "date": "2022-01-02", - "id": "slot-2", - "type": "ice", - "categories": ["competitive"], - "intervals": { - "09:00-10:00": { - "startTime": "09:00", - "endTime": "10:00" - }, - "09:00-11:00": { - "startTime": "09:00", - "endTime": "11:00" - }, - "10:00-11:00": { - "startTime": "10:00", - "endTime": "11:00" - } - }, - "notes": "" - }, - "slot-3": { - "date": "2021-12-31", - "id": "slot-2", - "type": "ice", - "categories": ["competitive"], - "intervals": { - "09:00-10:00": { - "startTime": "09:00", - "endTime": "10:00" - }, - "09:00-11:00": { - "startTime": "09:00", - "endTime": "11:00" - }, - "10:00-11:00": { - "startTime": "10:00", - "endTime": "11:00" - } - }, - "notes": "" - } - } -} diff --git a/packages/e2e/__testData__/slots/competitive.json b/packages/e2e/__testData__/slots/competitive.json new file mode 100644 index 000000000..456f26d54 --- /dev/null +++ b/packages/e2e/__testData__/slots/competitive.json @@ -0,0 +1,44 @@ +{ + "competitive-slot-1": { + "date": "2021-03-01", + "id": "competitive-slot-1", + "type": "ice", + "categories": ["competitive"], + "intervals": { + "08:00-09:00": { + "startTime": "08:00", + "endTime": "09:00" + }, + "08:30-09:30": { + "startTime": "08:30", + "endTime": "09:30" + }, + "08:00-10:00": { + "startTime": "08:00", + "endTime": "10:00" + } + }, + "notes": "" + }, + "competitive-slot-2": { + "date": "2021-03-01", + "id": "competitive-slot-2", + "type": "ice", + "categories": ["competitive"], + "intervals": { + "14:00-15:00": { + "startTime": "14:00", + "endTime": "15:00" + }, + "14:30-15:30": { + "startTime": "14:30", + "endTime": "15:30" + }, + "14:00-16:00": { + "startTime": "14:00", + "endTime": "16:00" + } + }, + "notes": "" + } +} diff --git a/packages/e2e/__testData__/slots/courseMinors.json b/packages/e2e/__testData__/slots/courseMinors.json new file mode 100644 index 000000000..b934cb5da --- /dev/null +++ b/packages/e2e/__testData__/slots/courseMinors.json @@ -0,0 +1,44 @@ +{ + "course-minors-slot-1": { + "date": "2021-03-01", + "id": "course-minors-slot-1", + "type": "ice", + "categories": ["course-minors"], + "intervals": { + "10:00-11:00": { + "startTime": "10:00", + "endTime": "11:00" + }, + "10:30-11:30": { + "startTime": "10:30", + "endTime": "11:30" + }, + "10:00-12:00": { + "startTime": "10:00", + "endTime": "12:00" + } + }, + "notes": "" + }, + "course-minors-slot-2": { + "date": "2021-03-01", + "id": "course-minors-slot-2", + "type": "ice", + "categories": ["course-minors"], + "intervals": { + "18:00-19:00": { + "startTime": "18:00", + "endTime": "19:00" + }, + "18:30-19:30": { + "startTime": "18:30", + "endTime": "19:30" + }, + "18:00-20:00": { + "startTime": "18:00", + "endTime": "20:00" + } + }, + "notes": "" + } +} diff --git a/packages/e2e/__testData__/slots/index.ts b/packages/e2e/__testData__/slots/index.ts new file mode 100644 index 000000000..c2859ff50 --- /dev/null +++ b/packages/e2e/__testData__/slots/index.ts @@ -0,0 +1,3 @@ +export { default as competitive } from "./competitive.json"; +export { default as courseMinors } from "./courseMinors.json"; +export { default as misc } from "./misc.json"; diff --git a/packages/e2e/__testData__/slots/misc.json b/packages/e2e/__testData__/slots/misc.json new file mode 100644 index 000000000..5c7933e5e --- /dev/null +++ b/packages/e2e/__testData__/slots/misc.json @@ -0,0 +1,65 @@ +{ + "slot-1": { + "date": "2022-01-01", + "id": "slot-1", + "type": "ice", + "categories": ["competitive", "course-adults"], + "intervals": { + "09:00-10:00": { + "startTime": "09:00", + "endTime": "10:00" + }, + "09:00-11:00": { + "startTime": "09:00", + "endTime": "11:00" + }, + "10:00-11:00": { + "startTime": "10:00", + "endTime": "11:00" + } + }, + "notes": "" + }, + "slot-2": { + "date": "2022-01-02", + "id": "slot-2", + "type": "ice", + "categories": ["competitive"], + "intervals": { + "09:00-10:00": { + "startTime": "09:00", + "endTime": "10:00" + }, + "09:00-11:00": { + "startTime": "09:00", + "endTime": "11:00" + }, + "10:00-11:00": { + "startTime": "10:00", + "endTime": "11:00" + } + }, + "notes": "" + }, + "slot-3": { + "date": "2021-12-31", + "id": "slot-2", + "type": "ice", + "categories": ["competitive"], + "intervals": { + "09:00-10:00": { + "startTime": "09:00", + "endTime": "10:00" + }, + "09:00-11:00": { + "startTime": "09:00", + "endTime": "11:00" + }, + "10:00-11:00": { + "startTime": "10:00", + "endTime": "11:00" + } + }, + "notes": "" + } +} diff --git a/packages/e2e/integration/attendance_spec.ts b/packages/e2e/integration/attendance_spec.ts index 4fe35ce78..b74896207 100644 --- a/packages/e2e/integration/attendance_spec.ts +++ b/packages/e2e/integration/attendance_spec.ts @@ -6,7 +6,7 @@ import { PrivateRoutes } from "@eisbuk/shared/ui"; import { customers } from "../__testData__/customers.json"; import { attendance } from "../__testData__/attendance.json"; -import { slots } from "../__testData__/slots.json"; +import { misc as slots } from "../__testData__/slots"; const gus = customers["gus"]; diff --git a/packages/e2e/integration/auth_redirect_spec.ts b/packages/e2e/integration/auth_redirect_spec.ts index b0bd1a3c6..9c05ab6ab 100644 --- a/packages/e2e/integration/auth_redirect_spec.ts +++ b/packages/e2e/integration/auth_redirect_spec.ts @@ -3,6 +3,7 @@ import { PrivateRoutes, Routes } from "@eisbuk/shared/ui"; import i18n, { ActionButton, AttendanceNavigationLabel, + AuthTitle, } from "@eisbuk/translations"; import { customers } from "../__testData__/customers.json"; @@ -97,46 +98,95 @@ describe("auth-related redirects", () => { // cy.contains(`${name} ${surname}`); }); - it("multiple secret keys: redirects to the first secretKey returned", () => { - // TEMP: The desired functionality is to not break the current behaviour (for now) - redirect to customer's bookings page. - // TODO: In on of the following PRs, here we will require the app to redirect to account selection. - - // Here, Morticia manages accounts for both herself and Wednesday. - // Meaning: there are more than one secretKey associated with her account. - // - // TEMP: With temp solution (redirect to first customer returned), we expect to be redirected to Morticia's customer area page. - // as 'morticia' (id) comes before 'wednesday' (id) lexicographically. - const { email, password, secretKey } = customers.morticia; + it("multiple secret keys: email: redirects to select account page from any of the private routes", () => { + // Here, Morticia manages accounts for both herself and Wednesday (using the email). + // Meaning: there are more than one secretKey associated with her email. + const { email, password } = customers.morticia; cy.signUp(email, password); // Check for /athletes page cy.visit(PrivateRoutes.Athletes); - cy.url().should("include", secretKey); + cy.url().should("include", Routes.SelectAccount); // Check for /athletes/new page cy.visit(PrivateRoutes.NewAthlete); - cy.url().should("include", secretKey); + cy.url().should("include", Routes.SelectAccount); // Check for /athletes/:id page cy.visit([PrivateRoutes.Athletes, customers.morticia.id].join("/")); - cy.url().should("include", secretKey); + cy.url().should("include", Routes.SelectAccount); // Check for attendance ("/") page cy.visit(PrivateRoutes.Root); - cy.url().should("include", secretKey); + cy.url().should("include", Routes.SelectAccount); // Check for slots page cy.visit(PrivateRoutes.Slots); - cy.url().should("include", secretKey); + cy.url().should("include", Routes.SelectAccount); // Check for admin preferences page cy.visit(PrivateRoutes.AdminPreferences); - cy.url().should("include", secretKey); + cy.url().should("include", Routes.SelectAccount); // If landing on 'customer_area' page, should automatically be redirected to their own customer area page (with their secret key) cy.visit(Routes.CustomerArea); - cy.url().should("include", secretKey); + cy.url().should("include", Routes.SelectAccount); + }); + + it("multiple secret keys: phone: redirects to select account page from any of the private routes", () => { + const { phone } = customers.erlich; + // Here, Erlich manages accounts for both himself and Jian Yang (using the phone). + // Meaning: there are more than one secretKey associated with his phone. + // + // Use the UI to log in using phone number (there's no other way to do this) + cy.visit("/"); + cy.clickButton(i18n.t(AuthTitle.SignInWithPhone)); + cy.contains(i18n.t(AuthTitle.SignInWithPhone) as string); + cy.getAttrWith("id", "dialCode").select("IT (+39)"); + cy.getAttrWith("id", "phone").type(phone.replace("+39", "")); + cy.clickButton(i18n.t(ActionButton.Verify)); + cy.contains(i18n.t(AuthTitle.EnterCode) as string); + cy.getRecaptchaCode(phone).then((code) => { + cy.getAttrWith("id", "code").type(code); + return cy.clickButton(i18n.t(ActionButton.Submit)); + }); + + // Check that the account selection page contains both accounts + // + // Names are broken up in to multiple rows, hence the one check per name/surname + cy.contains(customers.erlich.name); + cy.contains(customers.erlich.surname); + cy.contains(customers.yang.name); + cy.contains(customers.yang.surname); + + // Check for /athletes page + cy.visit(PrivateRoutes.Athletes); + cy.url().should("include", Routes.SelectAccount); + + // Check for /athletes/new page + cy.visit(PrivateRoutes.NewAthlete); + cy.url().should("include", Routes.SelectAccount); + + // Check for /athletes/:id page + cy.visit([PrivateRoutes.Athletes, customers.morticia.id].join("/")); + cy.url().should("include", Routes.SelectAccount); + + // Check for attendance ("/") page + cy.visit(PrivateRoutes.Root); + cy.url().should("include", Routes.SelectAccount); + + // Check for slots page + cy.visit(PrivateRoutes.Slots); + cy.url().should("include", Routes.SelectAccount); + + // Check for admin preferences page + cy.visit(PrivateRoutes.AdminPreferences); + cy.url().should("include", Routes.SelectAccount); + + // If landing on 'customer_area' page, should automatically be redirected to their own customer area page (with their secret key) + cy.visit(Routes.CustomerArea); + cy.url().should("include", Routes.SelectAccount); }); }); diff --git a/packages/e2e/integration/booked_intervals_spec.ts b/packages/e2e/integration/booked_intervals_spec.ts index 8691eba65..6f6a0166c 100644 --- a/packages/e2e/integration/booked_intervals_spec.ts +++ b/packages/e2e/integration/booked_intervals_spec.ts @@ -4,7 +4,7 @@ import { Customer, SlotInterface } from "@eisbuk/shared"; import { PrivateRoutes } from "@eisbuk/shared/ui"; import i18n, { SlotsAria } from "@eisbuk/translations"; -import { slots } from "../__testData__/slots.json"; +import { misc as slots } from "../__testData__/slots"; import { attendance } from "../__testData__/attendance.json"; import { customers } from "../__testData__/customers.json"; diff --git a/packages/e2e/integration/booking_spec_admin.ts b/packages/e2e/integration/booking_spec_admin.ts index 3690ff8b1..4d2b39d70 100644 --- a/packages/e2e/integration/booking_spec_admin.ts +++ b/packages/e2e/integration/booking_spec_admin.ts @@ -11,7 +11,7 @@ import i18n, { } from "@eisbuk/translations"; import { customers } from "../__testData__/customers.json"; -import { slots } from "../__testData__/slots.json"; +import { misc as slots } from "../__testData__/slots"; // extract saul from test data .json const saul = customers.saul as Customer; diff --git a/packages/e2e/integration/booking_spec_non_admin.ts b/packages/e2e/integration/booking_spec_non_admin.ts index 070973210..8ba1d5aad 100644 --- a/packages/e2e/integration/booking_spec_non_admin.ts +++ b/packages/e2e/integration/booking_spec_non_admin.ts @@ -15,7 +15,7 @@ import i18n, { import { customers } from "../__testData__/customers.json"; import { customers as saulWithExtendedDate } from "../__testData__/saul_with_extended_date.json"; -import { slots } from "../__testData__/slots.json"; +import { misc as slots } from "../__testData__/slots"; // extract saul from test data .json const saul = customers.saul as Customer; diff --git a/packages/e2e/integration/calendar_spec.ts b/packages/e2e/integration/calendar_spec.ts index c34c91324..4e915e9a7 100644 --- a/packages/e2e/integration/calendar_spec.ts +++ b/packages/e2e/integration/calendar_spec.ts @@ -8,7 +8,7 @@ import i18n, { NotificationMessage, } from "@eisbuk/translations"; -import { slots } from "../__testData__/slots.json"; +import { misc as slots } from "../__testData__/slots"; import { customers } from "../__testData__/customers.json"; import { customers as saulWithExtendedDate } from "../__testData__/saul_with_extended_date.json"; import { bookings } from "../__testData__/bookings.json"; diff --git a/packages/e2e/integration/copy_button_spec.ts b/packages/e2e/integration/copy_button_spec.ts index 44cd715c6..c100b2a21 100644 --- a/packages/e2e/integration/copy_button_spec.ts +++ b/packages/e2e/integration/copy_button_spec.ts @@ -4,7 +4,7 @@ import { SlotInterface } from "@eisbuk/shared"; import { PrivateRoutes } from "@eisbuk/shared/ui"; import i18n, { AdminAria, SlotsAria } from "@eisbuk/translations"; -import { slots } from "../__testData__/slots.json"; +import { misc as slots } from "../__testData__/slots"; const testDateLuxon = DateTime.fromISO("2022-01-01"); diff --git a/packages/e2e/integration/customer_profile_links_spec.ts b/packages/e2e/integration/customer_profile_links_spec.ts index 78ce5ceae..549a8c5fc 100644 --- a/packages/e2e/integration/customer_profile_links_spec.ts +++ b/packages/e2e/integration/customer_profile_links_spec.ts @@ -11,7 +11,7 @@ import i18n, { } from "@eisbuk/translations"; import { customers } from "../__testData__/customers.json"; -import { slots } from "../__testData__/slots.json"; +import { misc as slots } from "../__testData__/slots"; const { saul } = customers; diff --git a/packages/e2e/integration/multiple_accounts_spec.ts b/packages/e2e/integration/multiple_accounts_spec.ts new file mode 100644 index 000000000..f2fac6e0c --- /dev/null +++ b/packages/e2e/integration/multiple_accounts_spec.ts @@ -0,0 +1,174 @@ +import { DateTime } from "luxon"; + +import i18n, { AdminAria } from "@eisbuk/translations"; +import { Customer, SlotInterface } from "@eisbuk/shared"; +import { Routes } from "@eisbuk/shared/ui"; + +import { customers } from "../__testData__/customers.json"; +import * as testSlots from "__testData__/slots"; + +const { competitive, courseMinors } = testSlots as Record< + string, + Record +>; + +const slots = { + // Morticia is 'competitive' + ...competitive, + // Wednesday and Pugsley are 'course-minors' + ...courseMinors, +} as Record; + +const { morticia, wednesday, pugsley } = customers; + +describe("Management of multiple athletes with single account", () => { + // We're signing up/in as Morticia as her email is used to manege all three accounts + const { email, password } = morticia; + + beforeEach(() => + cy + .setClock(DateTime.fromISO("2021-02-01").toMillis()) + .initAdminApp() + .then((organization) => + Promise.all([ + cy.updateCustomers( + organization, + customers as Record + ), + cy.updateSlots(organization, slots), + ]) + ) + // We'll need Morticia to be signed in for all of the tests + .then(() => cy.signUp(email, password)) + ); + + it("naviagates between athletes managed by the given auth account", () => { + // Start by visiting the customer area page for Morticia + cy.visit([Routes.CustomerArea, morticia.secretKey].join("/")); + + // Navigate to March 2021 + cy.getAttrWith("aria-label", i18n.t(AdminAria.SeeFutureDates)).click(); + + cy.getSlotsDayContainer("2021-03-01").matchBookingsDay(competitive); + + // Open the account selection dropdown + cy.contains(`${morticia.name} ${morticia.surname}`).click(); + + // Check that the remaining two athletes are listed + cy.contains(`${wednesday.name} ${wednesday.surname}`); + cy.contains(`${pugsley.name} ${pugsley.surname}`); + + // Navigate to Wednesday's bookings + cy.contains(`${wednesday.name} ${wednesday.surname}`).click(); + + // Displays slots for Wednesday's (and Pugsley's) category (course-minors) + cy.getSlotsDayContainer("2021-03-01").matchBookingsDay(courseMinors); + + // Navigate to Pugsley's bookings + cy.contains(`${wednesday.name} ${wednesday.surname}`).click(); // Customer avatar menu + cy.contains(`${pugsley.name} ${pugsley.surname}`).click(); // Navigation + + cy.getSlotsDayContainer("2021-03-01").matchBookingsDay(courseMinors); + + // Navigate back to Morticia (for good measure) + cy.contains(`${pugsley.name} ${pugsley.surname}`).click(); // Customer avatar menu + cy.contains(`${morticia.name} ${morticia.surname}`).click(); // Navigation + + cy.getSlotsDayContainer("2021-03-01").matchBookingsDay(competitive); + }); + + // This tests entirely different slots per athlete as test data is set in such a way + // that there's no overlap between slots of different categories (and athletes in question are of different categories). + it("books for selected athlete (athletes with different categories)", () => { + // Start by visiting the customer area page for Morticia + cy.visit([Routes.CustomerArea, morticia.secretKey].join("/")); + + // Navigate to March 2021 + cy.getAttrWith("aria-label", i18n.t(AdminAria.SeeFutureDates)).click(); + + // Book two intervals (for Morticia) + cy.getSlotsDayContainer("2021-03-01").getBookingsCard("08:00-09:00").book(); + cy.getSlotsDayContainer("2021-03-01").getBookingsCard("14:00-16:00").book(); + + cy.getSlotsDayContainer("2021-03-01").matchBookingsDay(competitive, [ + "08:00-09:00", + "14:00-16:00", + ]); + + // Navigate to Wednesday's profile (no slots should be booked) + cy.contains(`${morticia.name} ${morticia.surname}`).click(); // Customer avatar menu + cy.contains(`${wednesday.name} ${wednesday.surname}`).click(); // Navigation + + cy.getSlotsDayContainer("2021-03-01").matchBookingsDay(courseMinors); + + // Book an interval (for Wednesday) + cy.getSlotsDayContainer("2021-03-01").getBookingsCard("10:30-11:30").book(); + + cy.getSlotsDayContainer("2021-03-01").matchBookingsDay(courseMinors, [ + "10:30-11:30", + ]); + + // Check for Wednesday's calendar bookings + cy.contains("Calendar").click(); + cy.matchCalendarMonth([{ date: "2021-03-01", interval: "10:30-11:30" }]); + + // Go back to Morticia's profile + cy.contains(`${wednesday.name} ${wednesday.surname}`).click(); // Customer avatar menu + cy.contains(`${morticia.name} ${morticia.surname}`).click(); // Navigation + + // Check Morticia's calendar + cy.matchCalendarMonth([ + { date: "2021-03-01", interval: "08:00-09:00" }, + { date: "2021-03-01", interval: "14:00-16:00" }, + ]); + }); + + // This tests for overlapping slots (customers are of same category) but ensures that the booking state changes between customer views. + it("books for selected athlete (athletes with different categories)", () => { + // Start by visiting the customer area page for Morticia + cy.visit([Routes.CustomerArea, morticia.secretKey].join("/")); + + // Navigate to March 2021 + cy.getAttrWith("aria-label", i18n.t(AdminAria.SeeFutureDates)).click(); + + // Navigate to Wednesday's profile and book two slots + cy.contains(`${morticia.name} ${morticia.surname}`).click(); // Customer avatar menu + cy.contains(`${wednesday.name} ${wednesday.surname}`).click(); // Navigation + + // Book an interval (for Wednesday) + cy.getSlotsDayContainer("2021-03-01").getBookingsCard("10:30-11:30").book(); + cy.getSlotsDayContainer("2021-03-01").getBookingsCard("18:00-20:00").book(); + + cy.getSlotsDayContainer("2021-03-01").matchBookingsDay(courseMinors, [ + "10:30-11:30", + "18:00-20:00", + ]); + + // Navigate to Pugsley's profile (should display the same intervals, but none should be booked) + cy.contains(`${wednesday.name} ${wednesday.surname}`).click(); // Customer avatar menu + cy.contains(`${pugsley.name} ${pugsley.surname}`).click(); // Navigation + + cy.getSlotsDayContainer("2021-03-01").matchBookingsDay(courseMinors); + + // Book a slot for Pugsley + cy.getSlotsDayContainer("2021-03-01").getBookingsCard("10:00-12:00").book(); + + cy.getSlotsDayContainer("2021-03-01").matchBookingsDay(courseMinors, [ + "10:00-12:00", + ]); + + // Check for Pugsley's calendar bookings + cy.contains("Calendar").click(); + cy.matchCalendarMonth([{ date: "2021-03-01", interval: "10:00-12:00" }]); + + // Go back to Wednesday's profile + cy.contains(`${pugsley.name} ${pugsley.surname}`).click(); // Customer avatar menu + cy.contains(`${wednesday.name} ${wednesday.surname}`).click(); // Navigation + + // Check for Wednesday's calendar bookings + cy.matchCalendarMonth([ + { date: "2021-03-01", interval: "10:30-11:30" }, + { date: "2021-03-01", interval: "18:00-20:00" }, + ]); + }); +}); diff --git a/packages/e2e/support/commands.ts b/packages/e2e/support/commands.ts index 68d453488..2f61ce509 100644 --- a/packages/e2e/support/commands.ts +++ b/packages/e2e/support/commands.ts @@ -1,5 +1,16 @@ -import { Customer } from "@eisbuk/shared"; -import i18n, { ActionButton } from "@eisbuk/translations"; +import { DateTime } from "luxon"; + +import { + Customer, + getSlotTimespan, + SlotInterface, + SlotInterval, + SlotType, + comparePeriodsEarliestFirst, + comparePeriodsLongestFirst, +} from "@eisbuk/shared"; +import { getIntervalString } from "@eisbuk/shared/ui"; +import i18n, { ActionButton, DateFormat } from "@eisbuk/translations"; import { TestID, @@ -17,6 +28,14 @@ type HttpRequestInterceptor = Extract< (req: any) => any >; +type IntervalCheck = SlotInterval & Partial>; + +interface MatchCalendarCardPayload { + date: string; + interval: IntervalCheck | string; + type?: SlotType; +} + // *********************************************************** // // When adding a new Command in command initializing procedure, add the @@ -109,6 +128,35 @@ declare global { url: string, cb: HttpRequestInterceptor ) => Chainable; + /** + * A matcher to get a slots day container element for the passed-in date + * @param date iso date + */ + getSlotsDayContainer: (date: string) => Chainable>; + /** + * Gets a booking card for a given interval. Please note that this will return all of the + * cards containing the same interval, so for best results, scope it under the day. + * + * @TODO matching all intervals is not ideal and, for now, we mitigate this by using test data + * with unique intervals. If the need should arise, this should be extended for more fine-grained control. + */ + getBookingsCard: (interval: string) => Chainable>; + /** A convenience method used to click a "Book" button within a bookings card. */ + book: () => Chainable>; + matchBookingsCard: ( + interval: IntervalCheck, + booked?: boolean + ) => Chainable>; + matchBookingsDay: ( + slots: Record, + bookedIntervals?: string[] + ) => Chainable>; + matchCalendarCard: ( + payload: MatchCalendarCardPayload + ) => Chainable>; + matchCalendarMonth: ( + intervals: MatchCalendarCardPayload[] + ) => Chainable>; } } } @@ -183,7 +231,123 @@ export default (): void => { // open new form cy.getByTestId("add-athlete").click(); }); - // #region CustomerForm + // #endregion CustomerForm + + // #region customer_bookings + Cypress.Commands.add( + "getSlotsDayContainer", + { prevSubject: "optional" }, + ($el, dateISO: string) => { + const container = $el ? cy.wrap($el) : cy.document(); + const date = DateTime.fromISO(dateISO); + let matcher = `[data-testid=${testId("slots-day-container")}]`; + matcher += `:has(:contains(${i18n.t(DateFormat.Full, { date })}))`; + return container.find(matcher); + } + ); + + Cypress.Commands.add( + "getBookingsCard", + { prevSubject: "optional" }, + ($el, interval: string) => { + const container = $el ? cy.wrap($el) : cy.document(); + let matcher = `[data-testid=${testId("booking-interval-card")}]`; + matcher += `:has(:contains(${getIntervalString(interval)}))`; + return container.find(matcher); + } + ); + Cypress.Commands.add("book", { prevSubject: "element" }, ($parent) => + cy.wrap($parent).clickButton(i18n.t(ActionButton.BookInterval)) + ); + + Cypress.Commands.add( + "matchBookingsCard", + { prevSubject: "element" }, + ($el, interval: IntervalCheck, booked = false) => { + const el = cy.wrap($el); + + el.should("contain", getIntervalString(interval)); + + if (interval.type) { + el.should("contain", interval.type); + } + + // Check only of 'booked' explicitly 'true' + if (booked === true) { + el.should("contain", i18n.t(ActionButton.Cancel) as string); + } + + // Check only of 'booked' explicitly 'false' + if (booked === false) { + el.should("contain", i18n.t(ActionButton.BookInterval) as string); + } + } + ); + + Cypress.Commands.add( + "matchBookingsDay", + { prevSubject: "element" }, + ( + $el, + slots: Record, + bookedIntervals: string[] = [] + ) => { + const intervals = slotsDayToIntervals(slots); + const numIntervals = intervals.length; + + const cardsMatcher = `[data-testid=${testId("booking-interval-card")}]`; + + cy.wrap($el).find(cardsMatcher).should("have.length", numIntervals); + + const _bookedIntervals = bookedIntervals.map(getIntervalString); + + intervals.forEach((interval, i) => { + const booked = _bookedIntervals.includes(getIntervalString(interval)); + cy.wrap($el) + .find(cardsMatcher) + .eq(i) + .matchBookingsCard(interval, booked); + }); + } + ); + + Cypress.Commands.add( + "matchCalendarCard", + { prevSubject: "element" }, + ($el, { date, interval, type }: MatchCalendarCardPayload) => { + const el = cy.wrap($el); + + el.should( + "contain", + i18n.t(DateFormat.Full, { date: DateTime.fromISO(date) }) + ); + el.should("contain", getIntervalString(interval)); + + if (type) { + el.should("contain", type); + } + } + ); + + Cypress.Commands.add( + "matchCalendarMonth", + { prevSubject: "optional" }, + ($el, intervals: MatchCalendarCardPayload[]) => { + const container = () => ($el ? cy.wrap($el) : cy.document()); + + const numIntervals = intervals.length; + + const cardsMatcher = `[data-testid=${testId("booking-calendar-card")}]`; + + container().find(cardsMatcher).should("have.length", numIntervals); + + intervals.forEach((interval, i) => + container().find(cardsMatcher).eq(i).matchCalendarCard(interval) + ); + } + ); + + // #endregion customer_bookings Cypress.Commands.add( "clearAndType", @@ -202,15 +366,22 @@ export default (): void => { .type(input + "\n") ); - Cypress.Commands.add("clickButton", (label: string, eq = 0) => { - cy - // include ':contains()' in the selector to retry the assertion until the element is found - // and prevent assertions on wrong buttons (in a render race condition kind of way) - .get(`button:contains(${label})`) - // if multiple elements found, get the specified 'eq' or fall back to first element found - .eq(eq) - .click({ force: true }); - }); + Cypress.Commands.add( + "clickButton", + { prevSubject: "optional" }, + ($el, label: string, eq = 0) => { + const container = $el ? cy.wrap($el) : cy.document(); + return ( + container + // include ':contains()' in the selector to retry the assertion until the element is found + // and prevent assertions on wrong buttons (in a render race condition kind of way) + .find(`button:contains(${label})`) + // if multiple elements found, get the specified 'eq' or fall back to first element found + .eq(eq) + .click({ force: true }) + ); + } + ); Cypress.Commands.add( "interceptTimes", @@ -225,3 +396,29 @@ export default (): void => { }) ); }; + +// #region helpers +/** + * Extracts intervals from the slot, for matching against the UI. + * The intervals are sorted in the same order as they will be renderd within the IntervalCardGroup. + */ +const slotToIntervals = (slot: SlotInterface): IntervalCheck[] => { + const { type, intervals } = slot; + return Object.values(intervals) + .map((interval) => ({ ...interval, type })) + .sort(comparePeriodsLongestFirst); +}; + +/** + * Takes in a full slots day, extracts intervals for each slot and sorts them in the same order + * as they should appear in SlotsDayContainer. + */ +const slotsDayToIntervals = (slots: Record) => + Object.values(slots) + .sort(({ intervals: i1 }, { intervals: i2 }) => { + const ts1 = getSlotTimespan(i1).replace(" ", ""); + const ts2 = getSlotTimespan(i2).replace(" ", ""); + return comparePeriodsEarliestFirst(ts1, ts2); + }) + .flatMap(slotToIntervals); +// #endregion helpers diff --git a/packages/e2e/support/index.ts b/packages/e2e/support/index.ts index e1f223d69..a5687ee0a 100644 --- a/packages/e2e/support/index.ts +++ b/packages/e2e/support/index.ts @@ -16,6 +16,9 @@ addFirebaseCommands(); // add our custom convenience commands initializeCommands(); +// Storing i18n.t as a variable as using the import in the stub sometimes causes stack size exceeded errors +const t = i18n.t; + // Overrides browser global Date Object to start from the first week of March 2021 // This means "new Date()" will always return Monday 1st March 2021 in all tests beforeEach(() => { @@ -25,7 +28,7 @@ beforeEach(() => { // stub i18n to use the i18n initialized in cypress so that both // our tests and app runtime are "on the same page" regarding i18n cy.stub(i18n, "t").callsFake((...args: Parameters) => - i18n.t(...args) + t(...args) ); cy.stub(translations, "useTranslation").callsFake(() => i18n.t); }); diff --git a/packages/react-redux-firebase-firestore/src/types.ts b/packages/react-redux-firebase-firestore/src/types.ts index 6fdfa5ed3..15659a4cd 100644 --- a/packages/react-redux-firebase-firestore/src/types.ts +++ b/packages/react-redux-firebase-firestore/src/types.ts @@ -34,6 +34,7 @@ type GetState = () => GlobalStateFragment; export interface SubscriptionMeta { organization: string; secretKey?: string; + secretKeys?: string[]; currentDate: DateTime; } @@ -138,7 +139,11 @@ export type CollectionSubscription = meta?: Record; } | { - collection: OrgSubCollection.Bookings | BookingSubCollection; + collection: OrgSubCollection.Bookings; + meta: { secretKeys: string[] }; + } + | { + collection: BookingSubCollection; meta: { secretKey: string }; }; diff --git a/packages/react-redux-firebase-firestore/src/utils/utils.ts b/packages/react-redux-firebase-firestore/src/utils/utils.ts index df31eff68..b763e6604 100644 --- a/packages/react-redux-firebase-firestore/src/utils/utils.ts +++ b/packages/react-redux-firebase-firestore/src/utils/utils.ts @@ -12,7 +12,7 @@ export const getConstraintForColl = ( collection: SubscriptionWhitelist, meta: SubscriptionMeta ): FirestoreListenerConstraint | null => { - const { organization, secretKey = "", currentDate } = meta; + const { organization, secretKeys = [], currentDate } = meta; // create date range constraint const startDateISO = currentDate @@ -38,7 +38,7 @@ export const getConstraintForColl = ( [Collection.Organizations]: { documents: [organization] }, [Collection.PublicOrgInfo]: { documents: [organization] }, [OrgSubCollection.Attendance]: { range }, - [OrgSubCollection.Bookings]: { documents: [secretKey] }, + [OrgSubCollection.Bookings]: { documents: secretKeys }, [OrgSubCollection.SlotsByDay]: { documents }, [OrgSubCollection.SlotBookingsCounts]: { documents }, [OrgSubCollection.Customers]: null, diff --git a/packages/shared/src/ui/enums/routes.ts b/packages/shared/src/ui/enums/routes.ts index 23f49316e..435abfff4 100644 --- a/packages/shared/src/ui/enums/routes.ts +++ b/packages/shared/src/ui/enums/routes.ts @@ -4,6 +4,7 @@ export enum Routes { Unauthorized = "/unautorized", SelfRegister = "/self_register", CustomerArea = "/customer_area", + SelectAccount = "/select_account", ErrorBoundary = "/do_trigger_an_error", AttendancePrintable = "/attendance_printable", Debug = "/debug", diff --git a/packages/shared/src/ui/index.ts b/packages/shared/src/ui/index.ts index 8f96e22e5..83092b4d9 100644 --- a/packages/shared/src/ui/index.ts +++ b/packages/shared/src/ui/index.ts @@ -1,3 +1,4 @@ export * from "./enums"; export * from "./hooks"; export * from "./data"; +export * from "./utils"; diff --git a/packages/shared/src/ui/utils.ts b/packages/shared/src/ui/utils.ts new file mode 100644 index 000000000..df535f9df --- /dev/null +++ b/packages/shared/src/ui/utils.ts @@ -0,0 +1,22 @@ +/** + * Processes an interval string to ensure the returned string is in format displayed by the UI + * (and can be used for matching in the UI). + * + * @example + * ```ts + * // Using misformatted string + * getDisplayInterval("10:00- 11:00") // "10:00 - 11:00" + * getDisplayInterval("10:00-11:00") // "10:00 - 11:00" + * getDisplayInterval("10:00 - 11:00") // "10:00 - 11:00" (ID) + * + * // Using interval object + * getDisplayInterval({ startTime: "10:00", endTime: "11:00" }) // "10:00 - 11:00" + * ``` + */ +export function getIntervalString( + interval: string | { startTime: string; endTime: string } +) { + return typeof interval === "string" + ? interval.replace(/ /g, "").replace("-", " - ") + : `${interval.startTime} - ${interval.endTime}`; +} diff --git a/packages/testing/src/testIds/index.ts b/packages/testing/src/testIds/index.ts index 034579874..f2fc8e7e1 100644 --- a/packages/testing/src/testIds/index.ts +++ b/packages/testing/src/testIds/index.ts @@ -35,6 +35,11 @@ type TestIDList = [ "input-dialog-email-input", // #endregion ConfirmDialog + // #region SlotsDayContainer + "slots-day-container", + "slots-day-content", + // #endregion SlotsDayContainer + // #region SlotOperationButtons "new-slot-button", "edit-slot-button", @@ -63,6 +68,7 @@ type TestIDList = [ // #region BookingCard "booking-interval-card", + "booking-calendar-card", "book-button", // #endregion BookingCard diff --git a/packages/ui/src/AthleteAvatarMenu/AthleteAvatarMenu.tsx b/packages/ui/src/AthleteAvatarMenu/AthleteAvatarMenu.tsx index c26e3f465..d89f5bed4 100644 --- a/packages/ui/src/AthleteAvatarMenu/AthleteAvatarMenu.tsx +++ b/packages/ui/src/AthleteAvatarMenu/AthleteAvatarMenu.tsx @@ -1,7 +1,8 @@ import React from "react"; import { Customer } from "@eisbuk/shared"; -import { /* Plus, */ PowerCircle } from "@eisbuk/svg"; +import { Plus, PowerCircle } from "@eisbuk/svg"; +import { ActionButton, useTranslation } from "@eisbuk/translations"; import { shortName } from "../utils/helpers"; @@ -20,49 +21,43 @@ const AthleteAvatarMenu: React.FC = ({ currentAthlete, otherAccounts, onAthleteClick = () => {}, - // onAddAccount = () => {}, + onAddAccount = () => {}, onLogout = () => {}, }) => { + const { t } = useTranslation(); + if (!currentAthlete) return null; return (
-
- {getDisplayName(currentAthlete)} -
+
{getDisplayName(currentAthlete)}
{otherAccounts?.length ? ( -
-

Other accounts

-
- {otherAccounts.map((profile) => ( - - ))} -
+
+ {otherAccounts.map((profile) => ( + + ))}
) : null} -
-

Actions

-
- {/* */} - -
+
+ +
); diff --git a/packages/ui/src/IntervalCard/BookingCardContainer.tsx b/packages/ui/src/IntervalCard/BookingCardContainer.tsx index 2d57e7dd8..6fe6666f7 100644 --- a/packages/ui/src/IntervalCard/BookingCardContainer.tsx +++ b/packages/ui/src/IntervalCard/BookingCardContainer.tsx @@ -1,6 +1,7 @@ import React from "react"; import { SlotType } from "@eisbuk/shared"; +import { testId } from "@eisbuk/testing/testIds"; import { BookingContainerProps, @@ -25,7 +26,11 @@ const BookingCardContainer: React.FC = ({ classes, ].join(" "); - return React.createElement(as, { className }, children); + return React.createElement( + as, + { className, "data-testid": testId("booking-interval-card") }, + children + ); }; const containerSizeLookup = { diff --git a/packages/ui/src/IntervalCard/CalendarCardContainer.tsx b/packages/ui/src/IntervalCard/CalendarCardContainer.tsx index 24ba69ede..5810b4720 100644 --- a/packages/ui/src/IntervalCard/CalendarCardContainer.tsx +++ b/packages/ui/src/IntervalCard/CalendarCardContainer.tsx @@ -3,6 +3,7 @@ import React, { useState } from "react"; import { SlotType } from "@eisbuk/shared"; import { CalendarContainerProps, CalendarContainerInnerProps } from "./types"; +import { testId } from "@eisbuk/testing/testIds"; // #region innerContainer export const CalendarCardExpandableContainer: React.FC< @@ -84,6 +85,10 @@ export const CalendarCardContainerInner: React.FC< classes, ].join(" "); - return React.createElement(as, { className }, children); + return React.createElement( + as, + { className, "data-testid": testId("booking-calendar-card") }, + children + ); }; // #endregion innerContainer diff --git a/packages/ui/src/IntervalCard/CardContent.tsx b/packages/ui/src/IntervalCard/CardContent.tsx index 21bc6fbc7..ada137d99 100644 --- a/packages/ui/src/IntervalCard/CardContent.tsx +++ b/packages/ui/src/IntervalCard/CardContent.tsx @@ -2,6 +2,7 @@ import React from "react"; import { DateTime } from "luxon"; import { useTranslation, DateFormat } from "@eisbuk/translations"; +import { getIntervalString } from "@eisbuk/shared/ui"; import { IntervalDuration, IntervalCardVariant } from "./types"; @@ -18,7 +19,7 @@ interface Props { } const CardContent: React.FC = ({ - interval: { startTime, endTime }, + interval, date: dateISO, variant = IntervalCardVariant.Booking, type, @@ -27,6 +28,7 @@ const CardContent: React.FC = ({ const { t } = useTranslation(); const date = DateTime.fromISO(dateISO); + const { startTime, endTime } = interval; const duration = calculateDuration(startTime, endTime); const dateString = ( @@ -36,7 +38,7 @@ const CardContent: React.FC = ({ ); const timestring = ( - {[startTime, endTime].join(" - ")} + {getIntervalString(interval)} ); const notesElement = ( diff --git a/packages/ui/src/SlotsDayContainer/SlotsDayContainer.tsx b/packages/ui/src/SlotsDayContainer/SlotsDayContainer.tsx index ca92150d2..9ca3669d7 100644 --- a/packages/ui/src/SlotsDayContainer/SlotsDayContainer.tsx +++ b/packages/ui/src/SlotsDayContainer/SlotsDayContainer.tsx @@ -1,6 +1,8 @@ import React from "react"; import { DateTime } from "luxon"; +import { testId } from "@eisbuk/testing/testIds"; + import i18n, { DateFormat } from "@eisbuk/translations"; interface SlotsDayContainerProps { @@ -28,7 +30,11 @@ const SlotsDayConatiner: React.FC = ({ ]; return ( -
+

{dateString} @@ -37,7 +43,10 @@ const SlotsDayConatiner: React.FC = ({ {additionalContent}

-
+
{children}