From d1a900cb91fce7e544dc8553b328bbaced4fcd1e Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 24 Nov 2024 20:05:56 +0400 Subject: [PATCH 01/71] refactor(tools): load reatom modules --- .eslintrc.json | 3 +- package-lock.json | 272 ++++++++++++++++++++++++++++++ package.json | 5 + src/app/providers/index.ts | 4 +- src/app/providers/withStore.tsx | 14 ++ src/shared/configs/const/env.ts | 2 + src/shared/configs/const/index.ts | 1 + src/shared/configs/context.ts | 11 ++ src/shared/configs/index.ts | 1 + 9 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 src/app/providers/withStore.tsx create mode 100644 src/shared/configs/const/env.ts create mode 100644 src/shared/configs/context.ts diff --git a/.eslintrc.json b/.eslintrc.json index 6cfe74be..0f7e6044 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,6 +14,7 @@ "plugin:import/warnings", "plugin:import/typescript", "plugin:boundaries/recommended", + "plugin:@reatom/recommended", "airbnb", "prettier" ], @@ -25,7 +26,7 @@ "ecmaVersion": "latest", "sourceType": "module" }, - "plugins": ["react", "@typescript-eslint", "effector"], + "plugins": ["react", "@typescript-eslint", "effector", "@reatom"], "rules": { /* STANDARD */ "no-use-before-define": "off", diff --git a/package-lock.json b/package-lock.json index 256d82dd..9ba15f58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "@mui/lab": "^5.0.0-alpha.108", "@mui/material": "^5.14.14", "@mui/x-date-pickers": "^6.16.2", + "@reatom/framework": "^3.4.55", + "@reatom/npm-react": "^3.10.2", "@withease/web-api": "^1.0.1", "atomic-router": "^0.8.0", "atomic-router-react": "^0.8.5", @@ -45,6 +47,9 @@ "@babel/core": "^7.23.2", "@faker-js/faker": "^8.4.1", "@playwright/test": "^1.40.1", + "@reatom/devtools": "^0.7.2", + "@reatom/eslint-plugin": "^3.4.3", + "@reatom/testing": "^3.4.7", "@rollup/plugin-babel": "^6.0.4", "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", @@ -3504,6 +3509,141 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@reatom/async": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/@reatom/async/-/async-3.16.3.tgz", + "integrity": "sha512-6dixuVInos/A1QaiICqCy0+IWTEx+Ae60Bv+hcSoU6Mo/23eC3OWh2F8unLTd4zmbR22nPRgS4U6NlUh33xHIg==", + "dependencies": { + "@reatom/core": "^3.5.0", + "@reatom/effects": "^3.10.0", + "@reatom/hooks": "^3.2.0", + "@reatom/primitives": "^3.5.0", + "@reatom/utils": "^3.11.0" + } + }, + "node_modules/@reatom/core": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@reatom/core/-/core-3.9.0.tgz", + "integrity": "sha512-ExlVfQ0rAkxnwWgbxWeNG4S5eNeTFpNlbFZMQHJ5MI+It7aYkqOie3Eh0ssk6AuvEPxzWM8+glsaZFQ6fPmTow==" + }, + "node_modules/@reatom/devtools": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@reatom/devtools/-/devtools-0.7.2.tgz", + "integrity": "sha512-joCQ1WEkjtiv7LpGqa91W5Ft3dYjpia43lCO55Oia9UPEa3In4EkAi32I6lGvZHQoe+q4xD4nbohbHCNn03ksQ==", + "dev": true + }, + "node_modules/@reatom/effects": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@reatom/effects/-/effects-3.10.1.tgz", + "integrity": "sha512-ZOopSf841e931JN/w92WNEYe2MJgaP6ncBCc5mLQTjXt0F7+Ry4oobYjWKRjwEreH7VrgPfOr3ZxCdQ9TKMzHg==", + "dependencies": { + "@reatom/core": "^3.2.0", + "@reatom/utils": "^3.5.0" + } + }, + "node_modules/@reatom/eslint-plugin": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@reatom/eslint-plugin/-/eslint-plugin-3.4.3.tgz", + "integrity": "sha512-2LelP4G1FXq+89pmWfKyNLV7GtxmmiHJfl2vac9oJU7lT+eqvmWi4Pd0duAJhWTLxvg9g5S1SLeGQ7tAw/gxeg==", + "dev": true + }, + "node_modules/@reatom/framework": { + "version": "3.4.55", + "resolved": "https://registry.npmjs.org/@reatom/framework/-/framework-3.4.55.tgz", + "integrity": "sha512-SWrD5DKS4zx49k6It2FPBJ4VlHJZom/hcGJgDUdPj4aRXfN738R6lw81Oun4kxY6p/WwxgCaEGfdG/Oh1H5gHg==", + "dependencies": { + "@reatom/async": "^3.16.3", + "@reatom/core": "^3.9.0", + "@reatom/effects": "^3.10.1", + "@reatom/hooks": "^3.6.0", + "@reatom/lens": "^3.11.5", + "@reatom/logger": "^3.8.4", + "@reatom/primitives": "^3.7.3", + "@reatom/utils": "^3.11.0" + } + }, + "node_modules/@reatom/hooks": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@reatom/hooks/-/hooks-3.6.0.tgz", + "integrity": "sha512-N3rqueSYvbQmuC2M2arF5Y7FHBK/rujK/gXXrVTTxXsetHMbwJAs7mxSB5a21s3eTHfCeOiuFCa8/qP7YL3ZVg==", + "dependencies": { + "@reatom/core": "^3.2.0", + "@reatom/effects": "^3.7.0", + "@reatom/utils": "^3.3.0" + } + }, + "node_modules/@reatom/lens": { + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/@reatom/lens/-/lens-3.11.5.tgz", + "integrity": "sha512-SWb+L696Hz0rlqW5e/ESkiKMXgTDYV0HAOk55rQiCsFekGWbnQEkHdzS6jAWFJlm7ZSzpJ2rs5wyns9EyU47jQ==", + "dependencies": { + "@reatom/core": "^3.4.0", + "@reatom/effects": "^3.2.0", + "@reatom/hooks": "^3.3.1", + "@reatom/primitives": "^3.6.0", + "@reatom/utils": "^3.1.0" + } + }, + "node_modules/@reatom/logger": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@reatom/logger/-/logger-3.8.4.tgz", + "integrity": "sha512-MOz8Td1eZV+kU4QpkZXAdO9qFtGjqpm40crIlMNweDtOH7GgUmV2oKgOXRORQzYbeGHVMlQHG4J5iPeEQdM7KA==", + "dependencies": { + "@reatom/core": "^3.8.1", + "@reatom/utils": "^3.9.0" + } + }, + "node_modules/@reatom/npm-react": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@reatom/npm-react/-/npm-react-3.10.2.tgz", + "integrity": "sha512-KJI0B/BVOijUAfPbBguTMSQAnoADPVU8A1JoFY9dhN8BgxXRrAIzhkQ1NJ7X9KqyAA2JUQTnKD/E0lAORqV0xQ==", + "dependencies": { + "@reatom/core": "^3.5.0", + "@reatom/effects": "^3.7.3", + "@reatom/lens": "^3.1.0", + "@reatom/utils": "^3.9.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@reatom/persist": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@reatom/persist/-/persist-3.4.1.tgz", + "integrity": "sha512-LM3JriTJNSH1EluVcvW9ik7DK5oa0NeIgkz8rIGvVk/c9ZIqp0Olthc/WEB5qNNDdzZMrebGRFiHRM2iO5/T6A==", + "dev": true, + "dependencies": { + "@reatom/core": "^3.3.0", + "@reatom/hooks": "^3.4.0", + "@reatom/lens": "^3.4.0", + "@reatom/utils": "^3.4.0" + } + }, + "node_modules/@reatom/primitives": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@reatom/primitives/-/primitives-3.7.3.tgz", + "integrity": "sha512-HzG2aoGzE2W410ZSqnT8hd21Xd+bNMIkN03tq7dWgZ4OAZKawVnxdeB8bHu+oA5ppxICfrY0HtPUUeQTbvxfgQ==", + "dependencies": { + "@reatom/core": "^3.1.1", + "@reatom/utils": "^3.1.1" + } + }, + "node_modules/@reatom/testing": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/@reatom/testing/-/testing-3.4.7.tgz", + "integrity": "sha512-MeduYtSWGXjdqOtDNUM0xXgoTS5VVjG9FzKZzzide4yT6kVwCUKFDya4lMfsf5kiZGMy+ojyGj5oq3EhEqxaAw==", + "dev": true, + "dependencies": { + "@reatom/core": "^3.5.0", + "@reatom/persist": "^3.3.0" + } + }, + "node_modules/@reatom/utils": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@reatom/utils/-/utils-3.11.0.tgz", + "integrity": "sha512-e59Gd7WC6jp5yo3LGUnveZkntsfNUJlXVkPGl24I2dFomoOH1XEPGJtDCzQQu5Y8S0mVuAUj5VwbqA4f+x4Pew==" + }, "node_modules/@rollup/plugin-babel": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz", @@ -15979,6 +16119,138 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, + "@reatom/async": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/@reatom/async/-/async-3.16.3.tgz", + "integrity": "sha512-6dixuVInos/A1QaiICqCy0+IWTEx+Ae60Bv+hcSoU6Mo/23eC3OWh2F8unLTd4zmbR22nPRgS4U6NlUh33xHIg==", + "requires": { + "@reatom/core": "^3.5.0", + "@reatom/effects": "^3.10.0", + "@reatom/hooks": "^3.2.0", + "@reatom/primitives": "^3.5.0", + "@reatom/utils": "^3.11.0" + } + }, + "@reatom/core": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@reatom/core/-/core-3.9.0.tgz", + "integrity": "sha512-ExlVfQ0rAkxnwWgbxWeNG4S5eNeTFpNlbFZMQHJ5MI+It7aYkqOie3Eh0ssk6AuvEPxzWM8+glsaZFQ6fPmTow==" + }, + "@reatom/devtools": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@reatom/devtools/-/devtools-0.7.2.tgz", + "integrity": "sha512-joCQ1WEkjtiv7LpGqa91W5Ft3dYjpia43lCO55Oia9UPEa3In4EkAi32I6lGvZHQoe+q4xD4nbohbHCNn03ksQ==", + "dev": true + }, + "@reatom/effects": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@reatom/effects/-/effects-3.10.1.tgz", + "integrity": "sha512-ZOopSf841e931JN/w92WNEYe2MJgaP6ncBCc5mLQTjXt0F7+Ry4oobYjWKRjwEreH7VrgPfOr3ZxCdQ9TKMzHg==", + "requires": { + "@reatom/core": "^3.2.0", + "@reatom/utils": "^3.5.0" + } + }, + "@reatom/eslint-plugin": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@reatom/eslint-plugin/-/eslint-plugin-3.4.3.tgz", + "integrity": "sha512-2LelP4G1FXq+89pmWfKyNLV7GtxmmiHJfl2vac9oJU7lT+eqvmWi4Pd0duAJhWTLxvg9g5S1SLeGQ7tAw/gxeg==", + "dev": true + }, + "@reatom/framework": { + "version": "3.4.55", + "resolved": "https://registry.npmjs.org/@reatom/framework/-/framework-3.4.55.tgz", + "integrity": "sha512-SWrD5DKS4zx49k6It2FPBJ4VlHJZom/hcGJgDUdPj4aRXfN738R6lw81Oun4kxY6p/WwxgCaEGfdG/Oh1H5gHg==", + "requires": { + "@reatom/async": "^3.16.3", + "@reatom/core": "^3.9.0", + "@reatom/effects": "^3.10.1", + "@reatom/hooks": "^3.6.0", + "@reatom/lens": "^3.11.5", + "@reatom/logger": "^3.8.4", + "@reatom/primitives": "^3.7.3", + "@reatom/utils": "^3.11.0" + } + }, + "@reatom/hooks": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@reatom/hooks/-/hooks-3.6.0.tgz", + "integrity": "sha512-N3rqueSYvbQmuC2M2arF5Y7FHBK/rujK/gXXrVTTxXsetHMbwJAs7mxSB5a21s3eTHfCeOiuFCa8/qP7YL3ZVg==", + "requires": { + "@reatom/core": "^3.2.0", + "@reatom/effects": "^3.7.0", + "@reatom/utils": "^3.3.0" + } + }, + "@reatom/lens": { + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/@reatom/lens/-/lens-3.11.5.tgz", + "integrity": "sha512-SWb+L696Hz0rlqW5e/ESkiKMXgTDYV0HAOk55rQiCsFekGWbnQEkHdzS6jAWFJlm7ZSzpJ2rs5wyns9EyU47jQ==", + "requires": { + "@reatom/core": "^3.4.0", + "@reatom/effects": "^3.2.0", + "@reatom/hooks": "^3.3.1", + "@reatom/primitives": "^3.6.0", + "@reatom/utils": "^3.1.0" + } + }, + "@reatom/logger": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@reatom/logger/-/logger-3.8.4.tgz", + "integrity": "sha512-MOz8Td1eZV+kU4QpkZXAdO9qFtGjqpm40crIlMNweDtOH7GgUmV2oKgOXRORQzYbeGHVMlQHG4J5iPeEQdM7KA==", + "requires": { + "@reatom/core": "^3.8.1", + "@reatom/utils": "^3.9.0" + } + }, + "@reatom/npm-react": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@reatom/npm-react/-/npm-react-3.10.2.tgz", + "integrity": "sha512-KJI0B/BVOijUAfPbBguTMSQAnoADPVU8A1JoFY9dhN8BgxXRrAIzhkQ1NJ7X9KqyAA2JUQTnKD/E0lAORqV0xQ==", + "requires": { + "@reatom/core": "^3.5.0", + "@reatom/effects": "^3.7.3", + "@reatom/lens": "^3.1.0", + "@reatom/utils": "^3.9.0", + "use-sync-external-store": "^1.2.0" + } + }, + "@reatom/persist": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@reatom/persist/-/persist-3.4.1.tgz", + "integrity": "sha512-LM3JriTJNSH1EluVcvW9ik7DK5oa0NeIgkz8rIGvVk/c9ZIqp0Olthc/WEB5qNNDdzZMrebGRFiHRM2iO5/T6A==", + "dev": true, + "requires": { + "@reatom/core": "^3.3.0", + "@reatom/hooks": "^3.4.0", + "@reatom/lens": "^3.4.0", + "@reatom/utils": "^3.4.0" + } + }, + "@reatom/primitives": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@reatom/primitives/-/primitives-3.7.3.tgz", + "integrity": "sha512-HzG2aoGzE2W410ZSqnT8hd21Xd+bNMIkN03tq7dWgZ4OAZKawVnxdeB8bHu+oA5ppxICfrY0HtPUUeQTbvxfgQ==", + "requires": { + "@reatom/core": "^3.1.1", + "@reatom/utils": "^3.1.1" + } + }, + "@reatom/testing": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/@reatom/testing/-/testing-3.4.7.tgz", + "integrity": "sha512-MeduYtSWGXjdqOtDNUM0xXgoTS5VVjG9FzKZzzide4yT6kVwCUKFDya4lMfsf5kiZGMy+ojyGj5oq3EhEqxaAw==", + "dev": true, + "requires": { + "@reatom/core": "^3.5.0", + "@reatom/persist": "^3.3.0" + } + }, + "@reatom/utils": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@reatom/utils/-/utils-3.11.0.tgz", + "integrity": "sha512-e59Gd7WC6jp5yo3LGUnveZkntsfNUJlXVkPGl24I2dFomoOH1XEPGJtDCzQQu5Y8S0mVuAUj5VwbqA4f+x4Pew==" + }, "@rollup/plugin-babel": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz", diff --git a/package.json b/package.json index 7b03c9eb..b30dced8 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "@mui/lab": "^5.0.0-alpha.108", "@mui/material": "^5.14.14", "@mui/x-date-pickers": "^6.16.2", + "@reatom/framework": "^3.4.55", + "@reatom/npm-react": "^3.10.2", "@withease/web-api": "^1.0.1", "atomic-router": "^0.8.0", "atomic-router-react": "^0.8.5", @@ -55,6 +57,9 @@ "@babel/core": "^7.23.2", "@faker-js/faker": "^8.4.1", "@playwright/test": "^1.40.1", + "@reatom/devtools": "^0.7.2", + "@reatom/eslint-plugin": "^3.4.3", + "@reatom/testing": "^3.4.7", "@rollup/plugin-babel": "^6.0.4", "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", diff --git a/src/app/providers/index.ts b/src/app/providers/index.ts index 7c73ea15..ef7dfe84 100644 --- a/src/app/providers/index.ts +++ b/src/app/providers/index.ts @@ -5,6 +5,7 @@ import { withGlobalStyles } from './withGlobalStyles'; import { withI18n } from './withI18n'; import { withNotifications } from './withNotifications'; import { withRouter } from './withRouter'; +import { withStore } from './withStore'; import { withStrictMode } from './withStrictMode'; export const withProviders = compose( @@ -13,5 +14,6 @@ export const withProviders = compose( withRouter, withGlobalStyles, withErrorBoundary, - withNotifications + withNotifications, + withStore ); diff --git a/src/app/providers/withStore.tsx b/src/app/providers/withStore.tsx new file mode 100644 index 00000000..40e7a256 --- /dev/null +++ b/src/app/providers/withStore.tsx @@ -0,0 +1,14 @@ +import { reatomContext } from '@reatom/npm-react'; +import { ComponentType } from 'react'; + +import { ctx } from '@/shared/configs'; + +export const withStore = + (Component: ComponentType): ComponentType => + () => { + return ( + + + + ); + }; diff --git a/src/shared/configs/const/env.ts b/src/shared/configs/const/env.ts new file mode 100644 index 00000000..dec32e71 --- /dev/null +++ b/src/shared/configs/const/env.ts @@ -0,0 +1,2 @@ +/* eslint-disable no-underscore-dangle */ +export const __DEV__ = import.meta.env.DEV; diff --git a/src/shared/configs/const/index.ts b/src/shared/configs/const/index.ts index 8cfab8f4..8c6b4cf7 100644 --- a/src/shared/configs/const/index.ts +++ b/src/shared/configs/const/index.ts @@ -2,3 +2,4 @@ export * from './routes'; export * from './forms'; export * from './api'; export * from './ui'; +export * from './env'; diff --git a/src/shared/configs/context.ts b/src/shared/configs/context.ts new file mode 100644 index 00000000..0ce4ec96 --- /dev/null +++ b/src/shared/configs/context.ts @@ -0,0 +1,11 @@ +import { connectDevtools } from '@reatom/devtools'; +import { createCtx, connectLogger } from '@reatom/framework'; + +import { __DEV__ } from './const'; + +export const ctx = createCtx(); + +if (__DEV__) { + connectLogger(ctx); + connectDevtools(ctx); +} diff --git a/src/shared/configs/index.ts b/src/shared/configs/index.ts index aaee642c..3c783e0b 100644 --- a/src/shared/configs/index.ts +++ b/src/shared/configs/index.ts @@ -1,3 +1,4 @@ export * from './routes'; export * from './const'; export * from './i18n'; +export * from './context'; From 841a2c9ffe88a3c751feca97a0f235b4db590411 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 24 Nov 2024 21:20:13 +0400 Subject: [PATCH 02/71] feat(shared): add base utilis to work with models --- src/shared/lib/construct-name.ts | 3 + src/shared/lib/contruct-name.spec.ts | 11 ++ .../lib/create-singleton-factory.spec.ts | 166 ++++++++++++++++++ src/shared/lib/create-singleton-factory.ts | 100 +++++++++++ src/shared/lib/extract-data.ts | 9 + src/shared/lib/index.ts | 4 + src/shared/lib/reconstruct-name.spec.ts | 26 +++ src/shared/lib/reconstruct-name.ts | 11 ++ src/shared/lib/retry-query.spec.ts | 154 ++++++++++++++++ src/shared/lib/retry-query.ts | 37 ++++ src/shared/types/common.ts | 1 + 11 files changed, 522 insertions(+) create mode 100644 src/shared/lib/construct-name.ts create mode 100644 src/shared/lib/contruct-name.spec.ts create mode 100644 src/shared/lib/create-singleton-factory.spec.ts create mode 100644 src/shared/lib/create-singleton-factory.ts create mode 100644 src/shared/lib/reconstruct-name.spec.ts create mode 100644 src/shared/lib/reconstruct-name.ts create mode 100644 src/shared/lib/retry-query.spec.ts create mode 100644 src/shared/lib/retry-query.ts diff --git a/src/shared/lib/construct-name.ts b/src/shared/lib/construct-name.ts new file mode 100644 index 00000000..2ed9dd67 --- /dev/null +++ b/src/shared/lib/construct-name.ts @@ -0,0 +1,3 @@ +export const constructName = (...parts: string[]): string => { + return parts.join('.'); +}; diff --git a/src/shared/lib/contruct-name.spec.ts b/src/shared/lib/contruct-name.spec.ts new file mode 100644 index 00000000..125c067f --- /dev/null +++ b/src/shared/lib/contruct-name.spec.ts @@ -0,0 +1,11 @@ +import { describe, expect, test } from 'vitest'; + +import { constructName } from './construct-name'; + +describe('src/shared/lib/contruct-name', () => { + test('should create model name', () => { + const name = constructName('grand', 'parent', 'child'); + + expect(name).toBe('grand.parent.child'); + }); +}); diff --git a/src/shared/lib/create-singleton-factory.spec.ts b/src/shared/lib/create-singleton-factory.spec.ts new file mode 100644 index 00000000..cdb6f4eb --- /dev/null +++ b/src/shared/lib/create-singleton-factory.spec.ts @@ -0,0 +1,166 @@ +import { noop } from '@reatom/framework'; +import { describe, expect, test, vi } from 'vitest'; + +import { AnyFunction, VoidFunction } from '../types'; + +import { + createSingletonFactory, + CreateSingletonFactoryOptions +} from './create-singleton-factory'; + +describe('src/shared/lib/create-singleton-factory', () => { + const params = { + param1: 'Hello', + param2: 'world', + }; + const factory = (params: any) => ({ params, name: 'original', }); + const anotherFactory = (params: any) => ({ params, name: 'another', }); + + const create = ( + factory: AnyFunction, + options?: CreateSingletonFactoryOptions + ) => { + return createSingletonFactory(factory, options); + }; + + describe('base usage', () => { + test('should return cached value', () => { + const cached = create(factory); + + const result = cached(params); + + expect(result).toStrictEqual({ + params, + name: 'original', + }); + }); + + test('should cache result returned with factory', () => { + const cached = create(factory); + + const result = cached(params); + + expect(result).toBe(cached(params)); + }); + + test('should return different values for different factories', () => { + const cached1 = create(factory); + const cached2 = create(anotherFactory); + + const result1 = cached1(params); + const result2 = cached2(params); + + expect(result1).not.toBe(result2); + }); + }); + + describe('custom key', () => { + test('should use passed constant key', () => { + const cached = create(factory, { + key: 'test', + }); + + const result = cached(params); + + expect(result).toBe(cached(params)); + expect(result).not.toBe(create(factory)(params)); + }); + + test('should use passed function to create key from params', () => { + const cached = create(factory, { + key: (params) => params.param1, + }); + + const result1 = cached(params); + const result2 = cached({ param1: 'world', }); + + expect(result1).toBe(cached(params)); + expect(result1).not.toBe(result2); + expect(result1).not.toBe(create(factory)(params)); + }); + }); + + describe('hooks', () => { + test('should stale value on callback of `staleOn` call', () => { + let cb: VoidFunction = noop; + + const cached = create(factory, { + hooks: { + staleOn: (_, callback) => { + cb = callback; + }, + }, + }); + + const result1 = cached(params); + + cb(); + + const result2 = cached(params); + + expect(result1).not.toBe(result2); + }); + + test('should setup stale hook only once for each result', () => { + const staleOn = vi.fn(); + + const cached = create(factory, { + hooks: { + staleOn, + }, + }); + + cached(params); + cached(params); + + expect(staleOn).toHaveBeenCalledTimes(1); + }); + + test('should write value on callback of `cacheOn` call', () => { + let cb: VoidFunction = noop; + + const cached = create(factory, { + hooks: { + cacheOn: (_, callback) => { + cb = callback; + }, + }, + }); + + const result1 = cached(params); + const result2 = cached(params); + + cb(); + + const result3 = cached(params); + + expect(result1).not.toBe(result2); + expect(result2).toBe(result3); + }); + }); + + describe('cache', () => { + test('should allows to pass custom cache', () => { + const cache = { + read: vi.fn(), + write: vi.fn(), + clear: vi.fn(), + }; + + const cached = create(factory, { + cache, + }); + + const result1 = cached(params); + + expect(cache.write).toHaveBeenCalledWith('factory', result1); + + cache.read.mockReturnValue(result1); + + cached(params); + + expect(cache.read).toHaveBeenCalledWith('factory'); + expect(cache.write).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/shared/lib/create-singleton-factory.ts b/src/shared/lib/create-singleton-factory.ts new file mode 100644 index 00000000..a7d4ea24 --- /dev/null +++ b/src/shared/lib/create-singleton-factory.ts @@ -0,0 +1,100 @@ +import { noop } from '@reatom/framework'; + +import { AnyFunction, VoidFunction } from '../types'; + +export type CacheKey = string; +export type GetCacheKey> = (...args: Args) => CacheKey; + +export interface Cache { + readonly read: (key: string) => Value | null; + readonly write: (key: string, value: Value) => void; + readonly clear: (key: string) => void; +} + +interface SingletonFactoryHooks { + readonly cacheOn?: (result: Result, callback: VoidFunction) => unknown; + readonly staleOn?: (result: Result, callback: VoidFunction) => unknown; +} + +export interface CreateSingletonFactoryOptions< + Args extends Array, + Result +> { + readonly key?: CacheKey | GetCacheKey; + readonly cache?: Cache; + readonly hooks?: SingletonFactoryHooks; +} + +export type CachedCreator, Result> = ( + ...args: Args +) => Result; + +/** + * Helper-function to create in-memory cache of singletons. + */ +const createInMemoryCache = (): Cache => { + // eslint-disable-next-line no-underscore-dangle + const _cache = new Map(); + + return { + read(key) { + return _cache.get(key) || null; + }, + write(key, value) { + _cache.set(key, value); + }, + clear(key) { + _cache.delete(key); + }, + }; +}; + +const defaultCacheOn: SingletonFactoryHooks['cacheOn'] = (_, cache) => + cache(); +const defaultStaleOn: SingletonFactoryHooks['staleOn'] = noop; + +/** + * Function to make from a simple factory a factory of singletons. + * Factory can be configured to create like only one singleton for whole app, + * as and several singletons for specific context (keep only one instance of result for each context) + * + * @remarks + * See examples in test cases + */ +export const createSingletonFactory = < + Factory extends AnyFunction, + Args extends Parameters = Parameters, + Result extends ReturnType = ReturnType +>( + factory: Factory, + options: CreateSingletonFactoryOptions = {} + ): Factory => { + const { + key = factory.name, + cache = createInMemoryCache(), + hooks = {}, + } = options; + const { cacheOn = defaultCacheOn, staleOn = defaultStaleOn, } = hooks; + + const getKey: GetCacheKey = typeof key === 'string' ? () => key : key; + + return ((...args: Args): Result => { + const key = getKey(...args); + + let result = cache.read(key); + + if (result) { + return result; + } + + result = factory(...args); + + cacheOn(result!, () => cache.write(key, result!)); + + const clear = () => cache.clear(key); + + staleOn(result!, clear); + + return result as Result; + }) as Factory; +}; diff --git a/src/shared/lib/extract-data.ts b/src/shared/lib/extract-data.ts index 8de5e371..236e2c66 100644 --- a/src/shared/lib/extract-data.ts +++ b/src/shared/lib/extract-data.ts @@ -1,3 +1,5 @@ +import { Ctx } from '@reatom/framework'; + import { StandardResponse } from '@/shared/types'; export const extractData = ({ @@ -7,3 +9,10 @@ export const extractData = ({ }): T => { return result.data; }; + +export const mapStandardResponse = ( + _ctx: Ctx, + data: StandardResponse +): T => { + return data.data; +}; diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index 1355a1eb..a8cd90db 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -12,3 +12,7 @@ export * from './chain-internal-route'; export * from './create-flag'; export * from './create-query-model'; export * from './create-popup-control-model'; +export * from './construct-name'; +export * from './reconstruct-name'; +export * from './create-singleton-factory'; +export * from './retry-query'; diff --git a/src/shared/lib/reconstruct-name.spec.ts b/src/shared/lib/reconstruct-name.spec.ts new file mode 100644 index 00000000..c71f86e1 --- /dev/null +++ b/src/shared/lib/reconstruct-name.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from 'vitest'; + +import { constructName } from './construct-name'; +import { reconstructName } from './reconstruct-name'; + +describe('reconstructName', () => { + test('should replace parts from end of passed name with passed parts', () => { + const oldName = constructName('grand', 'parent', 'child'); + + const parts = ['parent2', 'child2']; + + const name = reconstructName(oldName, ...parts); + + expect(name).toBe('grand.parent2.child2'); + }); + + test('should replace whole name with new parts if it longer than old one', () => { + const oldName = constructName('grand'); + + const parts = ['parent2', 'child2']; + + const name = reconstructName(oldName, ...parts); + + expect(name).toBe('parent2.child2'); + }); +}); diff --git a/src/shared/lib/reconstruct-name.ts b/src/shared/lib/reconstruct-name.ts new file mode 100644 index 00000000..df2082b0 --- /dev/null +++ b/src/shared/lib/reconstruct-name.ts @@ -0,0 +1,11 @@ +import { constructName } from './construct-name'; + +export const reconstructName = (name: string, ...parts: string[]): string => { + const nameParts = name.split('.'); + + const startIndex = Math.max(nameParts.length - parts.length, 0); + + nameParts.splice(startIndex, parts.length, ...parts); + + return constructName(...nameParts); +}; diff --git a/src/shared/lib/retry-query.spec.ts b/src/shared/lib/retry-query.spec.ts new file mode 100644 index 00000000..7b794452 --- /dev/null +++ b/src/shared/lib/retry-query.spec.ts @@ -0,0 +1,154 @@ +import { + AsyncAction, + Atom, + atom, + reatomAsync, + withRetry +} from '@reatom/framework'; +import { TestCtx, createTestCtx } from '@reatom/testing'; +import { vi, beforeEach, afterEach, expect, describe, test } from 'vitest'; + +import { WithRetry, retryQuery } from './retry-query'; + +vi.useFakeTimers(); + +describe('retryQuery()', () => { + let ctx: TestCtx; + const handler = vi.fn().mockResolvedValue(123); + let query: WithRetry; + let storeAtom: Atom; + + beforeEach(async () => { + query = reatomAsync(handler, 'query').pipe(withRetry()); + storeAtom = atom(0, 'storeAtom'); + ctx = createTestCtx(); + + await query(ctx); + }); + + afterEach(() => { + vi.clearAllTimers(); + vi.clearAllMocks(); + }); + + test('should retry query if store connected', async () => { + retryQuery({ + query, + store: storeAtom, + timeout: 1000, + }); + + const subscription = ctx.subscribeTrack(storeAtom); + + expect(handler).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(1000); + + expect(handler).toHaveBeenCalledTimes(2); + + subscription.unsubscribe(); + }); + + test('should do nothing if store is not connected', async () => { + retryQuery({ + query, + store: storeAtom, + timeout: 1000, + }); + + expect(handler).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(1000); + + expect(handler).toHaveBeenCalledTimes(1); + }); + + test('should stop query retries if store disconnected', async () => { + retryQuery({ + query, + store: storeAtom, + timeout: 1000, + }); + + const subscription = ctx.subscribeTrack(storeAtom); + + expect(handler).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(1000); + + expect(handler).toHaveBeenCalledTimes(2); + + subscription.unsubscribe(); + + await vi.advanceTimersByTimeAsync(1000); + + expect(handler).toHaveBeenCalledTimes(2); + }); + + test('should not start second timer on second connect', async () => { + retryQuery({ + query, + store: storeAtom, + timeout: 1000, + }); + + const subscription1 = ctx.subscribeTrack(storeAtom); + + await vi.advanceTimersByTimeAsync(500); + + const subscription2 = ctx.subscribeTrack(storeAtom); + + expect(handler).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(1000); + + expect(handler).toHaveBeenCalledTimes(2); + + subscription1.unsubscribe(); + subscription2.unsubscribe(); + }); + + test('should do nothing if query throw an error', async () => { + retryQuery({ + query, + store: storeAtom, + timeout: 1000, + }); + + handler.mockRejectedValueOnce(new Error('error')); + + const subscription = ctx.subscribeTrack(storeAtom); + + expect(handler).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(1000); + + expect(handler).toHaveBeenCalledTimes(2); + + subscription.unsubscribe(); + }); + + test('should call onFail if query throw an error', async () => { + const onFail = vi.fn(); + + retryQuery({ + query, + store: storeAtom, + onFail, + timeout: 1000, + }); + + handler.mockRejectedValueOnce(new Error('error')); + + const subscription = ctx.subscribeTrack(storeAtom); + + expect(handler).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(1000); + + expect(handler).toHaveBeenCalledTimes(2); + expect(onFail).toHaveBeenCalled(); + + subscription.unsubscribe(); + }); +}); diff --git a/src/shared/lib/retry-query.ts b/src/shared/lib/retry-query.ts new file mode 100644 index 00000000..c96353df --- /dev/null +++ b/src/shared/lib/retry-query.ts @@ -0,0 +1,37 @@ +/* eslint-disable no-await-in-loop */ +import { + Action, + ActionParams, + ActionPayload, + AsyncAction, + Atom, + noop, + onConnect, + sleep +} from '@reatom/framework'; + +import { AnyFunction } from '../types'; + +export type WithRetry = T & { + readonly paramsAtom: Atom | undefined>; + readonly retry: Action<[after?: number | undefined], ActionPayload>; + readonly retriesAtom: Atom; +}; + +export interface RetryQueryOptions { + readonly store: Atom; + readonly query: WithRetry; + readonly timeout: number; + readonly onFail?: AnyFunction; +} + +export const retryQuery = (options: RetryQueryOptions): void => { + const { query, store, timeout, onFail = noop, } = options; + + onConnect(store, async (ctx) => { + while (ctx.isConnected()) { + await ctx.schedule(() => sleep(timeout)); + await query.retry(ctx).catch(onFail); + } + }); +}; diff --git a/src/shared/types/common.ts b/src/shared/types/common.ts index 60223982..a53b3e00 100644 --- a/src/shared/types/common.ts +++ b/src/shared/types/common.ts @@ -6,6 +6,7 @@ export const hex = Template`#${String.withConstraint( )}`; export type HEX = Static; +export type AnyFunction = (...args: any) => any; export type VoidFunction = () => void; export interface ChainedParams { From 89121a9616f3195635912c1ae53fa69fd6e0e8ef Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 24 Nov 2024 21:40:31 +0400 Subject: [PATCH 03/71] refactor(activities): rewrite actions model --- package-lock.json | 3 +- package.json | 1 + .../activities/lib/use-activity-actions.ts | 18 ++++-- .../activities/model/actions/index.ts | 2 + .../activities/model/actions/model.spec.ts | 57 +++++++++++++++++++ .../activities/model/actions/model.ts | 45 +++++++++++++++ .../activities/model/actions/types.ts | 15 +++++ .../activities/model/activity-actions.ts | 27 --------- src/entities/activities/model/index.ts | 2 +- .../activities-actions-picker.tsx | 2 +- .../activity-action-picture.tsx | 3 +- .../activities-filters/filters.spec.tsx | 4 -- src/pages/room-activities/model.ts | 2 - src/shared/api/activities/requests.ts | 4 +- src/shared/api/activities/types.ts | 7 ++- src/shared/configs/const/env.ts | 2 + src/shared/configs/context.ts | 4 +- test-utils/fixtures/activities.ts | 6 +- test-utils/utils/render.tsx | 32 ++++++----- test-utils/utils/state-manager.ts | 12 +++- 20 files changed, 184 insertions(+), 64 deletions(-) create mode 100644 src/entities/activities/model/actions/index.ts create mode 100644 src/entities/activities/model/actions/model.spec.ts create mode 100644 src/entities/activities/model/actions/model.ts create mode 100644 src/entities/activities/model/actions/types.ts delete mode 100644 src/entities/activities/model/activity-actions.ts diff --git a/package-lock.json b/package-lock.json index 9ba15f58..fab47a8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@mui/x-date-pickers": "^6.16.2", "@reatom/framework": "^3.4.55", "@reatom/npm-react": "^3.10.2", + "@reatom/persist": "^3.4.1", "@withease/web-api": "^1.0.1", "atomic-router": "^0.8.0", "atomic-router-react": "^0.8.5", @@ -3612,7 +3613,6 @@ "version": "3.4.1", "resolved": "https://registry.npmjs.org/@reatom/persist/-/persist-3.4.1.tgz", "integrity": "sha512-LM3JriTJNSH1EluVcvW9ik7DK5oa0NeIgkz8rIGvVk/c9ZIqp0Olthc/WEB5qNNDdzZMrebGRFiHRM2iO5/T6A==", - "dev": true, "dependencies": { "@reatom/core": "^3.3.0", "@reatom/hooks": "^3.4.0", @@ -16219,7 +16219,6 @@ "version": "3.4.1", "resolved": "https://registry.npmjs.org/@reatom/persist/-/persist-3.4.1.tgz", "integrity": "sha512-LM3JriTJNSH1EluVcvW9ik7DK5oa0NeIgkz8rIGvVk/c9ZIqp0Olthc/WEB5qNNDdzZMrebGRFiHRM2iO5/T6A==", - "dev": true, "requires": { "@reatom/core": "^3.3.0", "@reatom/hooks": "^3.4.0", diff --git a/package.json b/package.json index b30dced8..b9d7e272 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@mui/x-date-pickers": "^6.16.2", "@reatom/framework": "^3.4.55", "@reatom/npm-react": "^3.10.2", + "@reatom/persist": "^3.4.1", "@withease/web-api": "^1.0.1", "atomic-router": "^0.8.0", "atomic-router-react": "^0.8.5", diff --git a/src/entities/activities/lib/use-activity-actions.ts b/src/entities/activities/lib/use-activity-actions.ts index 03b41c40..7768e9a1 100644 --- a/src/entities/activities/lib/use-activity-actions.ts +++ b/src/entities/activities/lib/use-activity-actions.ts @@ -1,7 +1,17 @@ -import { useUnit } from 'effector-react'; +import { useAtom } from '@reatom/npm-react'; -import { activityActionsModel } from '../model'; +import { ActivityActions, activityActionsModel } from '../model'; -export const useActivityActions = () => { - return useUnit(activityActionsModel.query); +export interface UseActivityActionsResult { + readonly data: ActivityActions; + readonly pending: boolean; +} + +export const useActivityActions = (): UseActivityActionsResult => { + const model = activityActionsModel.create(); + + const [data] = useAtom(model.actionsAtom); + const [pending] = useAtom(model.pendingAtom); + + return { data, pending, }; }; diff --git a/src/entities/activities/model/actions/index.ts b/src/entities/activities/model/actions/index.ts new file mode 100644 index 00000000..24c13f15 --- /dev/null +++ b/src/entities/activities/model/actions/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * as activityActionsModel from './model'; diff --git a/src/entities/activities/model/actions/model.spec.ts b/src/entities/activities/model/actions/model.spec.ts new file mode 100644 index 00000000..c47928f9 --- /dev/null +++ b/src/entities/activities/model/actions/model.spec.ts @@ -0,0 +1,57 @@ +import { beforeEach, describe, expect, test } from 'vitest'; + +import { create } from './model'; +import { ActivityActionsModel } from './types'; + +import { TestCtx, actions, createTestCtx, waitNextTick } from '~/test-utils'; + +describe('src/entities/activitites/models/actions/model', () => { + let ctx: TestCtx; + let model: ActivityActionsModel; + + const createModel = () => { + model = create(); + }; + + beforeEach(() => { + ctx = createTestCtx(); + }); + + test('should create signleton model', () => { + createModel(); + + const anotherModel = create(); + + expect(model).toBe(anotherModel); + }); + + test('should stale model only after last susbcriber unsubscribe', () => { + createModel(); + const anotherModel = create(); + + const track = ctx.subscribeTrack(model.actionsAtom); + const anotherTrack = ctx.subscribeTrack(anotherModel.actionsAtom); + + expect(model).toBe(anotherModel); + + anotherTrack.unsubscribe(); + + expect(model).toBe(create()); + + track.unsubscribe(); + + expect(model).not.toBe(create()); + }); + + test('should load all actions', async () => { + createModel(); + + const track = ctx.subscribeTrack(model.actionsAtom); + + await waitNextTick(); + + expect(track.lastInput()).toStrictEqual(actions); + + track.unsubscribe(); + }); +}); diff --git a/src/entities/activities/model/actions/model.ts b/src/entities/activities/model/actions/model.ts new file mode 100644 index 00000000..23219d85 --- /dev/null +++ b/src/entities/activities/model/actions/model.ts @@ -0,0 +1,45 @@ +import { + atom, + onDisconnect, + reatomResource, + withCache, + withDataAtom +} from '@reatom/framework'; + +import { activitiesApi } from '@/shared/api'; +import { + constructName, + createSingletonFactory, + mapStandardResponse +} from '@/shared/lib'; + +import { ActivityActions, ActivityActionsModel } from './types'; + +const modelName = constructName('activitites', 'actions'); + +export const create = createSingletonFactory( + (): ActivityActionsModel => { + const getActions = reatomResource(async (ctx) => { + return ctx.schedule(() => activitiesApi.getActions()); + }, constructName(modelName, 'getActions')).pipe( + withDataAtom([] as ActivityActions, mapStandardResponse), + withCache() + ); + + const pendingAtom = atom( + (ctx) => !!ctx.spy(getActions.pendingAtom), + constructName(modelName, 'pendingAtom') + ); + + return { + actionsAtom: getActions.dataAtom, + pendingAtom, + }; + }, + { + key: modelName, + hooks: { + staleOn: (result, stale) => onDisconnect(result.actionsAtom, stale), + }, + } +); diff --git a/src/entities/activities/model/actions/types.ts b/src/entities/activities/model/actions/types.ts new file mode 100644 index 00000000..53ea423e --- /dev/null +++ b/src/entities/activities/model/actions/types.ts @@ -0,0 +1,15 @@ +import { Atom } from '@reatom/framework'; +import { Number, Record, Static, String } from 'runtypes'; + +export const activityActionRT = Record({ + id: Number, + name: String, +}).asReadonly(); + +export interface ActivityAction extends Static {} +export type ActivityActions = ActivityAction[]; + +export interface ActivityActionsModel { + readonly actionsAtom: Atom; + readonly pendingAtom: Atom; +} diff --git a/src/entities/activities/model/activity-actions.ts b/src/entities/activities/model/activity-actions.ts deleted file mode 100644 index 0510beb4..00000000 --- a/src/entities/activities/model/activity-actions.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { cache, createQuery } from '@farfetched/core'; -import { runtypeContract } from '@farfetched/runtypes'; -import { createDomain } from 'effector'; -import { Array } from 'runtypes'; - -import { activitiesApi, ActivityAction, activityAction } from '@/shared/api'; -import { extractData } from '@/shared/lib'; -import { getStandardResponse, StandardResponse } from '@/shared/types'; - -const activityActions = createDomain(); - -const handlerFx = activityActions.effect(activitiesApi.getActions); - -export const query = createQuery< - void, - StandardResponse, - Error, - StandardResponse, - ActivityAction[] ->({ - initialData: [], - effect: handlerFx, - contract: runtypeContract(getStandardResponse(Array(activityAction))), - mapData: extractData, -}); - -cache(query); diff --git a/src/entities/activities/model/index.ts b/src/entities/activities/model/index.ts index f0866299..ea9d0744 100644 --- a/src/entities/activities/model/index.ts +++ b/src/entities/activities/model/index.ts @@ -1,3 +1,3 @@ +export * from './actions'; export * as activitiesInRoomModel from './activities-in-room'; -export * as activityActionsModel from './activity-actions'; export * as activitySpheresModel from './activity-spheres'; diff --git a/src/entities/activities/ui/activities-actions-picker/activities-actions-picker.tsx b/src/entities/activities/ui/activities-actions-picker/activities-actions-picker.tsx index 8268ce9b..fdf6b57d 100644 --- a/src/entities/activities/ui/activities-actions-picker/activities-actions-picker.tsx +++ b/src/entities/activities/ui/activities-actions-picker/activities-actions-picker.tsx @@ -8,12 +8,12 @@ import { import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { ActivityAction } from '@/shared/api'; import { preparePickerHandler, preparePickerSelectedValue } from '@/shared/lib'; import { CommonProps, PickerProps } from '@/shared/types'; import { Field, FieldProps } from '@/shared/ui'; import { useActivityActions } from '../../lib'; +import { ActivityAction } from '../../model'; import { ActivityActionPicture } from '../activity-action-picture'; export type ActivitiesActionsPickerProps = CommonProps & diff --git a/src/entities/activities/ui/activity-action-picture/activity-action-picture.tsx b/src/entities/activities/ui/activity-action-picture/activity-action-picture.tsx index 51625420..41601085 100644 --- a/src/entities/activities/ui/activity-action-picture/activity-action-picture.tsx +++ b/src/entities/activities/ui/activity-action-picture/activity-action-picture.tsx @@ -6,9 +6,10 @@ import cn from 'classnames'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { ActivityAction } from '@/shared/api'; import { CommonProps } from '@/shared/types'; +import { ActivityAction } from '../../model'; + import styles from './activity-action-picture.module.css'; export interface ActivityActionPictureProps diff --git a/src/features/activities/activities-filters/filters.spec.tsx b/src/features/activities/activities-filters/filters.spec.tsx index 00356303..a7150999 100644 --- a/src/features/activities/activities-filters/filters.spec.tsx +++ b/src/features/activities/activities-filters/filters.spec.tsx @@ -1,6 +1,5 @@ import { beforeEach, describe, expect, test } from 'vitest'; -import { activityActionsModel } from '@/entities/activities'; import { usersInRoomModel } from '@/entities/users'; import { deviceInfoModel } from '@/shared/models'; @@ -65,9 +64,6 @@ describe('features/activities/activities-filters/filters', () => { scope, params: { roomId, }, }); - await allSettled(activityActionsModel.query.start, { - scope, - }); await act(async () => createComponent()); }); diff --git a/src/pages/room-activities/model.ts b/src/pages/room-activities/model.ts index c76ac73c..8371408d 100644 --- a/src/pages/room-activities/model.ts +++ b/src/pages/room-activities/model.ts @@ -5,7 +5,6 @@ import { activitiesFiltersModel } from '@/features/activities'; import { activitiesInRoomModel, - activityActionsModel, activitySpheresModel } from '@/entities/activities'; import { roomModel, roomsModel } from '@/entities/rooms'; @@ -37,7 +36,6 @@ const queries = [ activitiesInRoomModel.query, usersInRoomModel.query, roomsModel.query, - activityActionsModel.query, activitySpheresModel.query ]; const sorting = { diff --git a/src/shared/api/activities/requests.ts b/src/shared/api/activities/requests.ts index 6dd88fd0..16296c52 100644 --- a/src/shared/api/activities/requests.ts +++ b/src/shared/api/activities/requests.ts @@ -4,7 +4,7 @@ import { instance, normalizeQuery } from '../request'; import { Activity, - ActivityAction, + ActivityActionDto, ActivitySphere, GetActivitiesInRoomParams } from './types'; @@ -23,7 +23,7 @@ export const getAll = async ({ export const getActions = async () => { return instance .get('activities/actions/all') - .json>(); + .json>(); }; export const getSpheres = async () => { diff --git a/src/shared/api/activities/types.ts b/src/shared/api/activities/types.ts index 26e894c9..8a010e85 100644 --- a/src/shared/api/activities/types.ts +++ b/src/shared/api/activities/types.ts @@ -9,12 +9,15 @@ import { import { user } from '../auth'; -export const activityAction = Record({ +const activityAction = Record({ id: Number, name: String, }).asReadonly(); -export interface ActivityAction extends Static {} +export interface ActivityActionDto { + readonly id: number; + readonly name: string; +} export const activitySphere = Record({ id: Number, diff --git a/src/shared/configs/const/env.ts b/src/shared/configs/const/env.ts index dec32e71..7be8fadf 100644 --- a/src/shared/configs/const/env.ts +++ b/src/shared/configs/const/env.ts @@ -1,2 +1,4 @@ /* eslint-disable no-underscore-dangle */ + export const __DEV__ = import.meta.env.DEV; +export const __TEST__ = import.meta.env.MODE === 'test'; diff --git a/src/shared/configs/context.ts b/src/shared/configs/context.ts index 0ce4ec96..644ebfcf 100644 --- a/src/shared/configs/context.ts +++ b/src/shared/configs/context.ts @@ -1,11 +1,11 @@ import { connectDevtools } from '@reatom/devtools'; import { createCtx, connectLogger } from '@reatom/framework'; -import { __DEV__ } from './const'; +import { __DEV__, __TEST__ } from './const'; export const ctx = createCtx(); -if (__DEV__) { +if (__DEV__ && !__TEST__) { connectLogger(ctx); connectDevtools(ctx); } diff --git a/test-utils/fixtures/activities.ts b/test-utils/fixtures/activities.ts index 87cac974..98f59c6f 100644 --- a/test-utils/fixtures/activities.ts +++ b/test-utils/fixtures/activities.ts @@ -1,6 +1,8 @@ -import { ActivityAction, ActivitySphere } from '@/shared/api'; +import { ActivityActions } from '@/entities/activities'; -export const actions: ActivityAction[] = [ +import { ActivitySphere } from '@/shared/api'; + +export const actions: ActivityActions = [ { id: 1, name: 'created', diff --git a/test-utils/utils/render.tsx b/test-utils/utils/render.tsx index d9d4f042..a670b275 100644 --- a/test-utils/utils/render.tsx +++ b/test-utils/utils/render.tsx @@ -2,6 +2,7 @@ import { Experimental_CssVarsProvider as CssVarsProvider } from '@mui/material'; import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { reatomContext } from '@reatom/npm-react'; import { render as rtlRender, RenderOptions as RTLRenderOptions, @@ -24,10 +25,11 @@ import React, { import { router as appRouter } from '@/shared/configs'; import { HistoryRouter } from './routing'; -import { fork, Scope } from './state-manager'; +import { createTestCtx, fork, Scope, TestCtx } from './state-manager'; interface CreateAllProvidersOptions { readonly scope: Scope; + readonly ctx: TestCtx; readonly router: HistoryRouter; readonly wrapper: JSXElementConstructor; } @@ -35,21 +37,23 @@ interface CreateAllProvidersOptions { const createAllProviders = ( options: CreateAllProvidersOptions ): ComponentType => { - const { router, scope, wrapper: Wrapper, } = options; + const { router, ctx, scope, wrapper: Wrapper, } = options; return (props) => { const { children, } = props; return ( - - - - - {children} - - - - + + + + + + {children} + + + + + ); }; }; @@ -67,10 +71,11 @@ const render = (ui: ReactNode, options: RenderOptions = {}): RenderResult => { scope = fork(), router = appRouter, wrapper = Fragment, + ctx = createTestCtx(), ...rest } = options; - const AllProviders = createAllProviders({ scope, router, wrapper, }); + const AllProviders = createAllProviders({ scope, router, wrapper, ctx, }); const defaultResult = rtlRender(ui, { ...rest, wrapper: AllProviders, }); @@ -97,10 +102,11 @@ const renderHook = ( scope = fork(), router = appRouter, wrapper = Fragment, + ctx = createTestCtx(), ...rest } = options; - const AllProviders = createAllProviders({ scope, router, wrapper, }); + const AllProviders = createAllProviders({ scope, router, wrapper, ctx, }); return rtlRenderHook(render, { ...rest, wrapper: AllProviders, }); }; diff --git a/test-utils/utils/state-manager.ts b/test-utils/utils/state-manager.ts index f03859f5..929873f9 100644 --- a/test-utils/utils/state-manager.ts +++ b/test-utils/utils/state-manager.ts @@ -1 +1,11 @@ -export { fork, Scope, scopeBind, allSettled } from 'effector'; +/* eslint-disable import/no-extraneous-dependencies */ +import { CtxOptions } from '@reatom/framework'; +import { TestCtx, createTestCtx as originCreateTestCtx } from '@reatom/testing'; + +const createTestCtx = (options?: CtxOptions): TestCtx => { + return originCreateTestCtx({ restrictMultipleContexts: false, ...options, }); +}; + +export * from '@reatom/testing'; +export { Scope, allSettled, fork, scopeBind } from 'effector'; +export { createTestCtx }; From 8dde23aa6adce0c17a06747cd9a3ba654f8b28ea Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 24 Nov 2024 21:55:28 +0400 Subject: [PATCH 04/71] refactor(activities): rename model dir to models --- src/entities/activities/index.ts | 2 +- src/entities/activities/{model => models}/actions/index.ts | 0 src/entities/activities/{model => models}/actions/model.spec.ts | 0 src/entities/activities/{model => models}/actions/model.ts | 0 src/entities/activities/{model => models}/actions/types.ts | 0 src/entities/activities/{model => models}/activities-in-room.ts | 0 src/entities/activities/{model => models}/activity-spheres.ts | 0 src/entities/activities/{model => models}/index.ts | 0 8 files changed, 1 insertion(+), 1 deletion(-) rename src/entities/activities/{model => models}/actions/index.ts (100%) rename src/entities/activities/{model => models}/actions/model.spec.ts (100%) rename src/entities/activities/{model => models}/actions/model.ts (100%) rename src/entities/activities/{model => models}/actions/types.ts (100%) rename src/entities/activities/{model => models}/activities-in-room.ts (100%) rename src/entities/activities/{model => models}/activity-spheres.ts (100%) rename src/entities/activities/{model => models}/index.ts (100%) diff --git a/src/entities/activities/index.ts b/src/entities/activities/index.ts index 1f437247..dfb222dc 100644 --- a/src/entities/activities/index.ts +++ b/src/entities/activities/index.ts @@ -1,3 +1,3 @@ export * from './ui'; -export * from './model'; +export * from './models'; export * from './lib'; diff --git a/src/entities/activities/model/actions/index.ts b/src/entities/activities/models/actions/index.ts similarity index 100% rename from src/entities/activities/model/actions/index.ts rename to src/entities/activities/models/actions/index.ts diff --git a/src/entities/activities/model/actions/model.spec.ts b/src/entities/activities/models/actions/model.spec.ts similarity index 100% rename from src/entities/activities/model/actions/model.spec.ts rename to src/entities/activities/models/actions/model.spec.ts diff --git a/src/entities/activities/model/actions/model.ts b/src/entities/activities/models/actions/model.ts similarity index 100% rename from src/entities/activities/model/actions/model.ts rename to src/entities/activities/models/actions/model.ts diff --git a/src/entities/activities/model/actions/types.ts b/src/entities/activities/models/actions/types.ts similarity index 100% rename from src/entities/activities/model/actions/types.ts rename to src/entities/activities/models/actions/types.ts diff --git a/src/entities/activities/model/activities-in-room.ts b/src/entities/activities/models/activities-in-room.ts similarity index 100% rename from src/entities/activities/model/activities-in-room.ts rename to src/entities/activities/models/activities-in-room.ts diff --git a/src/entities/activities/model/activity-spheres.ts b/src/entities/activities/models/activity-spheres.ts similarity index 100% rename from src/entities/activities/model/activity-spheres.ts rename to src/entities/activities/models/activity-spheres.ts diff --git a/src/entities/activities/model/index.ts b/src/entities/activities/models/index.ts similarity index 100% rename from src/entities/activities/model/index.ts rename to src/entities/activities/models/index.ts From 42e6fea9c701116463f3858c503f03e43f2ccae2 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 24 Nov 2024 22:04:07 +0400 Subject: [PATCH 05/71] refactor(activities): rewrite spheres model --- .../activities/lib/use-activity-actions.ts | 2 +- .../activities/lib/use-activity-spheres.ts | 21 ++++--- .../activities/models/activity-spheres.ts | 27 --------- src/entities/activities/models/index.ts | 2 +- .../activities/models/spheres/index.ts | 2 + .../activities/models/spheres/model.spec.ts | 57 +++++++++++++++++++ .../activities/models/spheres/model.ts | 45 +++++++++++++++ .../activities/models/spheres/types.ts | 15 +++++ .../activities-spheres-picker.tsx | 2 +- src/pages/room-activities/model.ts | 8 +-- src/shared/api/activities/requests.ts | 4 +- src/shared/api/activities/types.ts | 4 +- test-utils/fixtures/activities.ts | 8 +-- 13 files changed, 145 insertions(+), 52 deletions(-) delete mode 100644 src/entities/activities/models/activity-spheres.ts create mode 100644 src/entities/activities/models/spheres/index.ts create mode 100644 src/entities/activities/models/spheres/model.spec.ts create mode 100644 src/entities/activities/models/spheres/model.ts create mode 100644 src/entities/activities/models/spheres/types.ts diff --git a/src/entities/activities/lib/use-activity-actions.ts b/src/entities/activities/lib/use-activity-actions.ts index 7768e9a1..9801c324 100644 --- a/src/entities/activities/lib/use-activity-actions.ts +++ b/src/entities/activities/lib/use-activity-actions.ts @@ -1,6 +1,6 @@ import { useAtom } from '@reatom/npm-react'; -import { ActivityActions, activityActionsModel } from '../model'; +import { ActivityActions, activityActionsModel } from '../models'; export interface UseActivityActionsResult { readonly data: ActivityActions; diff --git a/src/entities/activities/lib/use-activity-spheres.ts b/src/entities/activities/lib/use-activity-spheres.ts index 8f3fa220..2dddac10 100644 --- a/src/entities/activities/lib/use-activity-spheres.ts +++ b/src/entities/activities/lib/use-activity-spheres.ts @@ -1,10 +1,17 @@ -import { useUnit } from 'effector-react'; +import { useAtom } from '@reatom/npm-react'; -import { activitySpheresModel } from '../model'; +import { ActivitySpheres, activitySpheresModel } from '../models'; -/** - * @deprecated - */ -export const useActivitySpheres = () => { - return useUnit(activitySpheresModel.query); +export interface UseActivitySpheresResult { + readonly data: ActivitySpheres; + readonly pending: boolean; +} + +export const useActivitySpheres = (): UseActivitySpheresResult => { + const model = activitySpheresModel.create(); + + const [data] = useAtom(model.spheresAtom); + const [pending] = useAtom(model.pendingAtom); + + return { data, pending, }; }; diff --git a/src/entities/activities/models/activity-spheres.ts b/src/entities/activities/models/activity-spheres.ts deleted file mode 100644 index caab5d3e..00000000 --- a/src/entities/activities/models/activity-spheres.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { cache, createQuery } from '@farfetched/core'; -import { runtypeContract } from '@farfetched/runtypes'; -import { createDomain } from 'effector'; -import { Array } from 'runtypes'; - -import { activitiesApi, ActivitySphere, activitySphere } from '@/shared/api'; -import { extractData } from '@/shared/lib'; -import { getStandardResponse, StandardResponse } from '@/shared/types'; - -const activitySpheres = createDomain(); - -const handlerFx = activitySpheres.effect(activitiesApi.getSpheres); - -export const query = createQuery< - void, - StandardResponse, - Error, - StandardResponse, - ActivitySphere[] ->({ - initialData: [], - effect: handlerFx, - contract: runtypeContract(getStandardResponse(Array(activitySphere))), - mapData: extractData, -}); - -cache(query); diff --git a/src/entities/activities/models/index.ts b/src/entities/activities/models/index.ts index ea9d0744..58568256 100644 --- a/src/entities/activities/models/index.ts +++ b/src/entities/activities/models/index.ts @@ -1,3 +1,3 @@ export * from './actions'; +export * from './spheres'; export * as activitiesInRoomModel from './activities-in-room'; -export * as activitySpheresModel from './activity-spheres'; diff --git a/src/entities/activities/models/spheres/index.ts b/src/entities/activities/models/spheres/index.ts new file mode 100644 index 00000000..db14c639 --- /dev/null +++ b/src/entities/activities/models/spheres/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * as activitySpheresModel from './model'; diff --git a/src/entities/activities/models/spheres/model.spec.ts b/src/entities/activities/models/spheres/model.spec.ts new file mode 100644 index 00000000..f3f4fbf4 --- /dev/null +++ b/src/entities/activities/models/spheres/model.spec.ts @@ -0,0 +1,57 @@ +import { beforeEach, describe, expect, test } from 'vitest'; + +import { create } from './model'; +import { ActivitySpheresModel } from './types'; + +import { TestCtx, spheres, createTestCtx, waitNextTick } from '~/test-utils'; + +describe('src/entities/activitites/models/spheres/model', () => { + let ctx: TestCtx; + let model: ActivitySpheresModel; + + const createModel = () => { + model = create(); + }; + + beforeEach(() => { + ctx = createTestCtx(); + }); + + test('should create signleton model', () => { + createModel(); + + const anotherModel = create(); + + expect(model).toBe(anotherModel); + }); + + test('should stale model only after last susbcriber unsubscribe', () => { + createModel(); + const anotherModel = create(); + + const track = ctx.subscribeTrack(model.spheresAtom); + const anotherTrack = ctx.subscribeTrack(anotherModel.spheresAtom); + + expect(model).toBe(anotherModel); + + anotherTrack.unsubscribe(); + + expect(model).toBe(create()); + + track.unsubscribe(); + + expect(model).not.toBe(create()); + }); + + test('should load all spheres', async () => { + createModel(); + + const track = ctx.subscribeTrack(model.spheresAtom); + + await waitNextTick(); + + expect(track.lastInput()).toStrictEqual(spheres); + + track.unsubscribe(); + }); +}); diff --git a/src/entities/activities/models/spheres/model.ts b/src/entities/activities/models/spheres/model.ts new file mode 100644 index 00000000..53ddc1b6 --- /dev/null +++ b/src/entities/activities/models/spheres/model.ts @@ -0,0 +1,45 @@ +import { + atom, + onDisconnect, + reatomResource, + withCache, + withDataAtom +} from '@reatom/framework'; + +import { activitiesApi } from '@/shared/api'; +import { + constructName, + createSingletonFactory, + mapStandardResponse +} from '@/shared/lib'; + +import { ActivitySpheres, ActivitySpheresModel } from './types'; + +const modelName = constructName('activitites', 'spheres'); + +export const create = createSingletonFactory( + (): ActivitySpheresModel => { + const getSpheres = reatomResource(async (ctx) => { + return ctx.schedule(() => activitiesApi.getSpheres()); + }, constructName(modelName, 'getSpheres')).pipe( + withDataAtom([] as ActivitySpheres, mapStandardResponse), + withCache() + ); + + const pendingAtom = atom( + (ctx) => !!ctx.spy(getSpheres.pendingAtom), + constructName(modelName, 'pendingAtom') + ); + + return { + spheresAtom: getSpheres.dataAtom, + pendingAtom, + }; + }, + { + key: modelName, + hooks: { + staleOn: (result, stale) => onDisconnect(result.spheresAtom, stale), + }, + } +); diff --git a/src/entities/activities/models/spheres/types.ts b/src/entities/activities/models/spheres/types.ts new file mode 100644 index 00000000..adaa6663 --- /dev/null +++ b/src/entities/activities/models/spheres/types.ts @@ -0,0 +1,15 @@ +import { Atom } from '@reatom/framework'; +import { Number, Record, Static, String } from 'runtypes'; + +export const activitySphereRT = Record({ + id: Number, + name: String, +}).asReadonly(); + +export interface ActivitySphere extends Static {} +export type ActivitySpheres = ActivitySphere[]; + +export interface ActivitySpheresModel { + readonly spheresAtom: Atom; + readonly pendingAtom: Atom; +} diff --git a/src/entities/activities/ui/activities-spheres-picker/activities-spheres-picker.tsx b/src/entities/activities/ui/activities-spheres-picker/activities-spheres-picker.tsx index f06216d0..c279232b 100644 --- a/src/entities/activities/ui/activities-spheres-picker/activities-spheres-picker.tsx +++ b/src/entities/activities/ui/activities-spheres-picker/activities-spheres-picker.tsx @@ -2,12 +2,12 @@ import { Autocomplete, Chip, ListItem, ListItemText } from '@mui/material'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { ActivitySphere } from '@/shared/api'; import { preparePickerHandler, preparePickerSelectedValue } from '@/shared/lib'; import { CommonProps, PickerProps } from '@/shared/types'; import { Field, FieldProps } from '@/shared/ui'; import { useActivitySpheres } from '../../lib'; +import { ActivitySphere } from '../../models'; export type ActivitiesSpheresPickerProps = CommonProps & PickerProps & diff --git a/src/pages/room-activities/model.ts b/src/pages/room-activities/model.ts index 8371408d..7adcbc9a 100644 --- a/src/pages/room-activities/model.ts +++ b/src/pages/room-activities/model.ts @@ -3,10 +3,7 @@ import { createEvent, sample } from 'effector'; import { activitiesFiltersModel } from '@/features/activities'; -import { - activitiesInRoomModel, - activitySpheresModel -} from '@/entities/activities'; +import { activitiesInRoomModel } from '@/entities/activities'; import { roomModel, roomsModel } from '@/entities/rooms'; import { usersInRoomModel } from '@/entities/users'; @@ -35,8 +32,7 @@ const formApplied = createEvent(); const queries = [ activitiesInRoomModel.query, usersInRoomModel.query, - roomsModel.query, - activitySpheresModel.query + roomsModel.query ]; const sorting = { by: 'createdAt', diff --git a/src/shared/api/activities/requests.ts b/src/shared/api/activities/requests.ts index 16296c52..631ee54a 100644 --- a/src/shared/api/activities/requests.ts +++ b/src/shared/api/activities/requests.ts @@ -5,7 +5,7 @@ import { instance, normalizeQuery } from '../request'; import { Activity, ActivityActionDto, - ActivitySphere, + ActivitySphereDto, GetActivitiesInRoomParams } from './types'; @@ -29,5 +29,5 @@ export const getActions = async () => { export const getSpheres = async () => { return instance .get('activities/spheres/all') - .json>(); + .json>(); }; diff --git a/src/shared/api/activities/types.ts b/src/shared/api/activities/types.ts index 8a010e85..32303b58 100644 --- a/src/shared/api/activities/types.ts +++ b/src/shared/api/activities/types.ts @@ -19,12 +19,12 @@ export interface ActivityActionDto { readonly name: string; } -export const activitySphere = Record({ +const activitySphere = Record({ id: Number, name: String, }).asReadonly(); -export interface ActivitySphere extends Static {} +export interface ActivitySphereDto extends Static {} export const activity = Record({ id: Number, diff --git a/test-utils/fixtures/activities.ts b/test-utils/fixtures/activities.ts index 98f59c6f..cfea512d 100644 --- a/test-utils/fixtures/activities.ts +++ b/test-utils/fixtures/activities.ts @@ -1,8 +1,6 @@ -import { ActivityActions } from '@/entities/activities'; +import { ActivityActionDto, ActivitySphereDto } from '@/shared/api'; -import { ActivitySphere } from '@/shared/api'; - -export const actions: ActivityActions = [ +export const actions: ActivityActionDto[] = [ { id: 1, name: 'created', @@ -17,7 +15,7 @@ export const actions: ActivityActions = [ } ]; -export const spheres: ActivitySphere[] = [ +export const spheres: ActivitySphereDto[] = [ { id: 1, name: 'task', From d85e9c9a98c3556d2567a7e7edc0684c8d131e29 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 24 Nov 2024 23:42:15 +0400 Subject: [PATCH 06/71] refactor(activities): rewrite activities model for activities in room --- src/entities/activities/lib/index.ts | 1 + .../activities/lib/use-activities-model.ts | 9 ++ .../activities/models/actions/types.ts | 1 + .../activities/models/activities-in-room.ts | 50 -------- .../activities/models/activities/index.ts | 2 + .../models/activities/model.spec.ts | 112 ++++++++++++++++++ .../activities/models/activities/model.ts | 95 +++++++++++++++ .../activities/models/activities/types.ts | 58 +++++++++ src/entities/activities/models/index.ts | 2 +- .../activities/models/spheres/types.ts | 2 + 10 files changed, 281 insertions(+), 51 deletions(-) create mode 100644 src/entities/activities/lib/use-activities-model.ts delete mode 100644 src/entities/activities/models/activities-in-room.ts create mode 100644 src/entities/activities/models/activities/index.ts create mode 100644 src/entities/activities/models/activities/model.spec.ts create mode 100644 src/entities/activities/models/activities/model.ts create mode 100644 src/entities/activities/models/activities/types.ts diff --git a/src/entities/activities/lib/index.ts b/src/entities/activities/lib/index.ts index ac7f95af..f652cd5a 100644 --- a/src/entities/activities/lib/index.ts +++ b/src/entities/activities/lib/index.ts @@ -1,2 +1,3 @@ export * from './use-activity-actions'; export * from './use-activity-spheres'; +export * from './use-activities-model'; diff --git a/src/entities/activities/lib/use-activities-model.ts b/src/entities/activities/lib/use-activities-model.ts new file mode 100644 index 00000000..8bd41a5b --- /dev/null +++ b/src/entities/activities/lib/use-activities-model.ts @@ -0,0 +1,9 @@ +import { useMemo } from 'react'; + +import { ActivitiesModel, activititesModel } from '../models'; + +export const useActivitiesModel = (roomId: number): ActivitiesModel => { + return useMemo(() => { + return activititesModel.create({ roomId, }); + }, [roomId]); +}; diff --git a/src/entities/activities/models/actions/types.ts b/src/entities/activities/models/actions/types.ts index 53ea423e..0740fb01 100644 --- a/src/entities/activities/models/actions/types.ts +++ b/src/entities/activities/models/actions/types.ts @@ -7,6 +7,7 @@ export const activityActionRT = Record({ }).asReadonly(); export interface ActivityAction extends Static {} +export type ActivityActionId = ActivityAction['id']; export type ActivityActions = ActivityAction[]; export interface ActivityActionsModel { diff --git a/src/entities/activities/models/activities-in-room.ts b/src/entities/activities/models/activities-in-room.ts deleted file mode 100644 index fe04d6ff..00000000 --- a/src/entities/activities/models/activities-in-room.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { cache, createQuery, keepFresh } from '@farfetched/core'; -import { runtypeContract } from '@farfetched/runtypes'; -import { createDomain } from 'effector'; -import { interval } from 'patronum'; - -import { - Activity, - activity, - GetActivitiesInRoomParams, - activitiesApi -} from '@/shared/api'; -import { extractData } from '@/shared/lib'; -import { - StandardResponse, - getStandardResponse, - PaginationResponse, - getPaginationResponse -} from '@/shared/types'; - -const activitiesDomain = createDomain(); -const handlerFx = activitiesDomain.effect< - GetActivitiesInRoomParams, - StandardResponse> ->(activitiesApi.getAll); - -export const query = createQuery< - GetActivitiesInRoomParams, - StandardResponse>, - Error, - StandardResponse>, - PaginationResponse ->({ - initialData: { items: [], totalCount: 0, limit: 50, }, - effect: handlerFx, - contract: runtypeContract( - getStandardResponse(getPaginationResponse(activity)) - ), - mapData: extractData, -}); - -export const $hasItems = query.$data.map((data) => !!data.totalCount); -export const $pageCount = query.$data.map((data) => - Math.ceil(data.totalCount / data.limit) -); - -cache(query); - -keepFresh(query, { - triggers: [interval({ timeout: 5000, })], -}); diff --git a/src/entities/activities/models/activities/index.ts b/src/entities/activities/models/activities/index.ts new file mode 100644 index 00000000..98fdb41d --- /dev/null +++ b/src/entities/activities/models/activities/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * as activititesModel from './model'; diff --git a/src/entities/activities/models/activities/model.spec.ts b/src/entities/activities/models/activities/model.spec.ts new file mode 100644 index 00000000..30bfdf8e --- /dev/null +++ b/src/entities/activities/models/activities/model.spec.ts @@ -0,0 +1,112 @@ +import { take, takeNested } from '@reatom/framework'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +import { create } from './model'; +import { ActivitiesModel } from './types'; + +import { + TestCtx, + createTestCtx, + defaultRoom, + activities, + actions, + rooms +} from '~/test-utils'; + +describe('src/entities/activitites/models/activities/model', () => { + const defaultRoomId = defaultRoom.id; + + let ctx: TestCtx; + let model: ActivitiesModel; + + const createModel = (roomId = defaultRoomId) => { + model = create({ roomId, }); + }; + + beforeEach(() => { + ctx = createTestCtx(); + + vi.useFakeTimers(); + }); + + afterEach(async () => { + await vi.runOnlyPendingTimersAsync(); + vi.useRealTimers(); + }); + + test('should create signleton model for the same room', () => { + createModel(); + + const anotherModel = create({ roomId: defaultRoomId, }); + + expect(model).toBe(anotherModel); + }); + + test('should create different models for different rooms', () => { + createModel(); + + const anotherModel = create({ roomId: rooms[1].id, }); + + expect(model).not.toBe(anotherModel); + }); + + test('should create new model for the same room if old one has been unused', () => { + createModel(); + + const track = ctx.subscribeTrack(model.activititesAtom); + + track.unsubscribe(); + + expect(model).not.toBe(create({ roomId: defaultRoomId, })); + }); + + test('should load all activitites', async () => { + createModel(); + + const track = ctx.subscribeTrack(model.activititesAtom); + + await take(ctx, model.activititesAtom); + + expect(track.lastInput()).toStrictEqual(activities); + expect(ctx.get(model.hasItemsAtom)).toBeTruthy(); + expect(ctx.get(model.pagesCountAtom)).toBe(2); + + track.unsubscribe(); + }); + + test('should fetch new data on fetch action call', async () => { + createModel(); + + const track = ctx.subscribeTrack(model.activititesAtom); + + await take(ctx, model.activititesAtom); + + await takeNested(ctx, model.fetch, { actionIds: [actions[1].id], }); + + const newActivities = activities.filter( + (activity) => activity.action.id === actions[1].id + ); + + expect(track.lastInput()).toStrictEqual(newActivities); + expect(ctx.get(model.hasItemsAtom)).toBe(!!newActivities.length); + expect(ctx.get(model.pagesCountAtom)).toBe(2); + + track.unsubscribe(); + }); + + test('should refresh activities every 5 sec', async () => { + createModel(); + + const track = ctx.subscribeTrack(model.activititesAtom); + + await take(ctx, model.activititesAtom); + + const promise = take(ctx, model.activititesAtom); + + await vi.advanceTimersByTimeAsync(5000); + + await expect(promise).resolves.toStrictEqual(activities); + + track.unsubscribe(); + }); +}); diff --git a/src/entities/activities/models/activities/model.ts b/src/entities/activities/models/activities/model.ts new file mode 100644 index 00000000..5ae7da8e --- /dev/null +++ b/src/entities/activities/models/activities/model.ts @@ -0,0 +1,95 @@ +import { + withDataAtom, + withCache, + atom, + onDisconnect, + withRetry, + reatomAsync, + onConnect, + withAbort +} from '@reatom/framework'; + +import { activitiesApi } from '@/shared/api'; +import { + constructName, + createSingletonFactory, + mapStandardResponse, + retryQuery +} from '@/shared/lib'; +import { PaginationResponse } from '@/shared/types'; + +import { + ActivitiesModel, + Activity, + CreateActivitiesModelParams, + FetchActivititesParams +} from './types'; + +const modelName = constructName('activitites', 'in-room'); + +export const create = createSingletonFactory( + (params: CreateActivitiesModelParams): ActivitiesModel => { + const { roomId, } = params; + + const fetch = reatomAsync(async (ctx, params?: FetchActivititesParams) => { + return ctx.schedule(() => + activitiesApi.getAll({ ...params, roomId, }, ctx.controller.signal) + ); + }, constructName(modelName, 'fetch')).pipe( + withDataAtom( + { items: [], totalCount: 0, limit: 50, } as PaginationResponse, + mapStandardResponse + ), + withCache(), + withRetry(), + withAbort() + ); + + const pendingAtom = atom( + (ctx) => !!ctx.spy(fetch.pendingAtom), + constructName(modelName, 'pendingAtom') + ); + + const activititesAtom = atom( + (ctx) => ctx.spy(fetch.dataAtom).items, + constructName(modelName, 'activititesAtom') + ); + + const hasItemsAtom = atom( + (ctx) => !!ctx.spy(fetch.dataAtom).totalCount, + constructName(modelName, 'hasItemsAtom') + ); + + const pagesCountAtom = atom((ctx) => { + const { limit, totalCount, } = ctx.spy(fetch.dataAtom); + + return Math.ceil(totalCount / limit); + }, constructName(modelName, 'pagesCountAtom')); + + onConnect(fetch.dataAtom, (ctx) => { + fetch(ctx); + + return () => fetch.abort(ctx); + }); + + retryQuery({ + query: fetch, + store: activititesAtom, + timeout: 5000, + }); + + return { + fetch, + activititesAtom, + pagesCountAtom, + hasItemsAtom, + pendingAtom, + }; + }, + { + key: (params) => constructName(modelName, params.roomId.toString()), + hooks: { + staleOn: (result, stale) => onDisconnect(result.activititesAtom, stale), + }, + } +); diff --git a/src/entities/activities/models/activities/types.ts b/src/entities/activities/models/activities/types.ts new file mode 100644 index 00000000..62b5fd83 --- /dev/null +++ b/src/entities/activities/models/activities/types.ts @@ -0,0 +1,58 @@ +import { AsyncAction, Atom } from '@reatom/framework'; +import { Number, Record, Static, String } from 'runtypes'; + +import { user } from '@/shared/api'; +import { + PaginationResponse, + SortDirection, + StandardResponse +} from '@/shared/types'; + +import { ActivityActionId, activityActionRT } from '../actions'; +import { ActivitySphereId, activitySphereRT } from '../spheres'; + + + +export const activityRT = Record({ + id: Number, + roomId: Number, + activist: user, + action: activityActionRT, + sphere: activitySphereRT, + createdAt: String, +}).asReadonly(); + +export interface Activity extends Static {} +export type ActivityId = Activity['id']; + +export type Activities = Activity[]; + +export interface FetchActivititesParams { + readonly page?: number; + readonly count?: number; + readonly by?: string | null; + readonly type?: SortDirection | null; + readonly before?: string | null; + readonly after?: string | null; + /** + * @todo Extract into new type + */ + readonly activistIds?: number[]; + readonly sphereIds?: ActivitySphereId[]; + readonly actionIds?: ActivityActionId[]; +} + +export interface CreateActivitiesModelParams { + readonly roomId: number; +} + +export interface ActivitiesModel { + readonly fetch: AsyncAction< + [params?: FetchActivititesParams], + StandardResponse> + >; + readonly activititesAtom: Atom; + readonly pagesCountAtom: Atom; + readonly hasItemsAtom: Atom; + readonly pendingAtom: Atom; +} diff --git a/src/entities/activities/models/index.ts b/src/entities/activities/models/index.ts index 58568256..908381d6 100644 --- a/src/entities/activities/models/index.ts +++ b/src/entities/activities/models/index.ts @@ -1,3 +1,3 @@ export * from './actions'; export * from './spheres'; -export * as activitiesInRoomModel from './activities-in-room'; +export * from './activities'; diff --git a/src/entities/activities/models/spheres/types.ts b/src/entities/activities/models/spheres/types.ts index adaa6663..d4199870 100644 --- a/src/entities/activities/models/spheres/types.ts +++ b/src/entities/activities/models/spheres/types.ts @@ -7,6 +7,8 @@ export const activitySphereRT = Record({ }).asReadonly(); export interface ActivitySphere extends Static {} +export type ActivitySphereId = ActivitySphere['id']; + export type ActivitySpheres = ActivitySphere[]; export interface ActivitySpheresModel { From 0afbba906d6fd3f5635ae155170752130ece1b61 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 24 Nov 2024 23:43:02 +0400 Subject: [PATCH 07/71] test(tools): add controller mock to return filtered activities --- test-utils/fixtures/activities.ts | 36 +++++++++- test-utils/mock-server/handlres/actions.ts | 21 ------ test-utils/mock-server/handlres/activities.ts | 65 +++++++++++++++++++ test-utils/mock-server/handlres/index.ts | 6 +- .../utils/create-pagination-response.ts | 16 +++++ test-utils/mock-server/utils/index.ts | 1 + 6 files changed, 120 insertions(+), 25 deletions(-) delete mode 100644 test-utils/mock-server/handlres/actions.ts create mode 100644 test-utils/mock-server/handlres/activities.ts create mode 100644 test-utils/mock-server/utils/create-pagination-response.ts diff --git a/test-utils/fixtures/activities.ts b/test-utils/fixtures/activities.ts index cfea512d..7477ed57 100644 --- a/test-utils/fixtures/activities.ts +++ b/test-utils/fixtures/activities.ts @@ -1,4 +1,11 @@ -import { ActivityActionDto, ActivitySphereDto } from '@/shared/api'; +import { + ActivityActionDto, + ActivityDto, + ActivitySphereDto +} from '@/shared/api'; + +import { defaultRoom } from './rooms'; +import { defaultUser } from './users'; export const actions: ActivityActionDto[] = [ { @@ -29,3 +36,30 @@ export const spheres: ActivitySphereDto[] = [ name: 'tag', } ]; + +export const activities: ActivityDto[] = [ + { + id: 1, + action: actions[0], + sphere: spheres[0], + roomId: defaultRoom.id, + activist: defaultUser, + createdAt: '2022-11-12T12:28:01', + }, + { + id: 2, + action: actions[1], + sphere: spheres[0], + roomId: defaultRoom.id, + activist: defaultUser, + createdAt: '2022-11-13T12:28:01', + }, + { + id: 3, + action: actions[1], + sphere: spheres[1], + roomId: defaultRoom.id, + activist: defaultUser, + createdAt: '2022-11-13T14:28:01', + } +]; diff --git a/test-utils/mock-server/handlres/actions.ts b/test-utils/mock-server/handlres/actions.ts deleted file mode 100644 index 9e2945fb..00000000 --- a/test-utils/mock-server/handlres/actions.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -import { http } from 'msw'; - -import { actions, spheres } from '../../fixtures'; -import { BASE_URL } from '../constants'; -import { createStandardResponse, createUrl } from '../utils'; - -const baseUrl = createUrl(BASE_URL, 'activities'); -const getActionsUrl = createUrl(baseUrl, 'actions', 'all'); -const getSpheresUrl = createUrl(baseUrl, 'spheres', 'all'); - -export const success = { - getActions: http.get(getActionsUrl, () => { - return createStandardResponse(actions); - }), - getSpheres: http.get(getSpheresUrl, () => { - return createStandardResponse(spheres); - }), -}; - -export const standard = Object.values(success); diff --git a/test-utils/mock-server/handlres/activities.ts b/test-utils/mock-server/handlres/activities.ts new file mode 100644 index 00000000..cc0dddf2 --- /dev/null +++ b/test-utils/mock-server/handlres/activities.ts @@ -0,0 +1,65 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { http } from 'msw'; + +import { actions, activities, spheres } from '../../fixtures'; +import { BASE_URL } from '../constants'; +import { + createPaginationResponse, + createStandardResponse, + createUrl +} from '../utils'; + +const baseUrl = createUrl(BASE_URL, 'activities'); +const getAllUrl = createUrl(baseUrl, ':roomId'); +const getActionsUrl = createUrl(baseUrl, 'actions', 'all'); +const getSpheresUrl = createUrl(baseUrl, 'spheres', 'all'); + +function filterActivities( + roomId: string, + actionIds: string[], + sphereIds: string[], + activistIds: string[] +) { + return activities.filter((activity) => { + return ( + activity.roomId === +roomId && + (actionIds.length + ? actionIds.includes(activity.action.id.toString()) + : true) && + (sphereIds.length + ? sphereIds.includes(activity.sphere.id.toString()) + : true) && + (activistIds.length + ? activistIds.includes(activity.activist.id.toString()) + : true) + ); + }); +} + +export const success = { + getActions: http.get(getActionsUrl, () => { + return createStandardResponse(actions); + }), + getSpheres: http.get(getSpheresUrl, () => { + return createStandardResponse(spheres); + }), + getAll: http.get(getAllUrl, ({ params, request, }) => { + const { roomId, } = params; + const url = new URL(request.url); + const actionIds = (url.searchParams.getAll('actionIds') ?? []) as string[]; + const sphereIds = (url.searchParams.getAll('sphereIds') ?? []) as string[]; + const activistIds = (url.searchParams.getAll('activistIds') ?? + []) as string[]; + + const filtered = filterActivities( + roomId as string, + actionIds, + sphereIds, + activistIds + ); + + return createPaginationResponse(filtered); + }), +}; + +export const standard = Object.values(success); diff --git a/test-utils/mock-server/handlres/index.ts b/test-utils/mock-server/handlres/index.ts index 133c07b8..0d076b2b 100644 --- a/test-utils/mock-server/handlres/index.ts +++ b/test-utils/mock-server/handlres/index.ts @@ -1,4 +1,4 @@ -import * as actions from './actions'; +import * as activities from './activities'; import * as auth from './auth'; import * as invitations from './invitations'; import * as members from './members'; @@ -8,7 +8,7 @@ import * as tasks from './tasks'; import * as users from './users'; export const standardHandlers = [ - ...actions.standard, + ...activities.standard, ...auth.standard, ...invitations.standard, ...members.standard, @@ -19,7 +19,7 @@ export const standardHandlers = [ ]; export const handlers = { - actions, + activities, auth, invitations, members, diff --git a/test-utils/mock-server/utils/create-pagination-response.ts b/test-utils/mock-server/utils/create-pagination-response.ts new file mode 100644 index 00000000..55ec59b0 --- /dev/null +++ b/test-utils/mock-server/utils/create-pagination-response.ts @@ -0,0 +1,16 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { HttpResponse } from 'msw'; + +import { createStandardResponse } from './create-standard-response'; + +export const createPaginationResponse = ( + items: unknown[], + limit = 50, + totalCount = items.length + limit +): HttpResponse => { + return createStandardResponse({ + items, + limit, + totalCount, + }); +}; diff --git a/test-utils/mock-server/utils/index.ts b/test-utils/mock-server/utils/index.ts index 6e27e1ab..f7ed41b1 100644 --- a/test-utils/mock-server/utils/index.ts +++ b/test-utils/mock-server/utils/index.ts @@ -1,3 +1,4 @@ export * from './create-standard-response'; +export * from './create-pagination-response'; export * from './create-url'; export * from './errors'; From 1efe662941a97625f11ed2ad7ec143c282040969 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 24 Nov 2024 23:48:06 +0400 Subject: [PATCH 08/71] refactor(activities): rename dto types --- src/shared/api/activities/requests.ts | 21 ++++++++------- src/shared/api/activities/types.ts | 38 +++++++++------------------ 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/src/shared/api/activities/requests.ts b/src/shared/api/activities/requests.ts index 631ee54a..99e51c1b 100644 --- a/src/shared/api/activities/requests.ts +++ b/src/shared/api/activities/requests.ts @@ -3,31 +3,32 @@ import { PaginationResponse, StandardResponse } from '@/shared/types'; import { instance, normalizeQuery } from '../request'; import { - Activity, + ActivityDto, ActivityActionDto, ActivitySphereDto, GetActivitiesInRoomParams } from './types'; -export const getAll = async ({ - roomId, - ...query -}: GetActivitiesInRoomParams) => { +export const getAll = async ( + { roomId, ...query }: GetActivitiesInRoomParams, + signal?: AbortSignal +) => { return instance .get(`activities/${roomId}`, { searchParams: new URLSearchParams(normalizeQuery(query)), + signal, }) - .json>>(); + .json>>(); }; -export const getActions = async () => { +export const getActions = async (signal?: AbortSignal) => { return instance - .get('activities/actions/all') + .get('activities/actions/all', { signal, }) .json>(); }; -export const getSpheres = async () => { +export const getSpheres = async (signal?: AbortSignal) => { return instance - .get('activities/spheres/all') + .get('activities/spheres/all', { signal, }) .json>(); }; diff --git a/src/shared/api/activities/types.ts b/src/shared/api/activities/types.ts index 32303b58..825727df 100644 --- a/src/shared/api/activities/types.ts +++ b/src/shared/api/activities/types.ts @@ -1,5 +1,3 @@ -import { Number, Record, Static, String } from 'runtypes'; - import { DatesFiltersParams, InRoomParams, @@ -7,35 +5,25 @@ import { SortParams } from '@/shared/types'; -import { user } from '../auth'; - -const activityAction = Record({ - id: Number, - name: String, -}).asReadonly(); +import { User } from '../auth'; export interface ActivityActionDto { readonly id: number; readonly name: string; } +export interface ActivitySphereDto { + readonly id: number; + readonly name: string; +} -const activitySphere = Record({ - id: Number, - name: String, -}).asReadonly(); - -export interface ActivitySphereDto extends Static {} - -export const activity = Record({ - id: Number, - roomId: Number, - activist: user, - action: activityAction, - sphere: activitySphere, - createdAt: String, -}).asReadonly(); - -export interface Activity extends Static {} +export interface ActivityDto { + readonly id: number; + readonly roomId: number; + readonly activist: User; + readonly action: ActivityActionDto; + readonly sphere: ActivitySphereDto; + readonly createdAt: string; +} export interface GetActivitiesInRoomParams extends InRoomParams, From 1beed3fe8ff2bef1b464a4840599bd51db6fef0a Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 24 Nov 2024 23:49:59 +0400 Subject: [PATCH 09/71] test(activities): add tests for activitites list items --- .lintstagedrc.json | 2 +- .stylelintrc.json | 1 - .../activity-action-icon.spec.tsx.snap | 115 ++++++++++++++++++ .../activity-action-icon.module.css} | 0 .../activity-action-icon.module.css.d.ts} | 0 .../activity-action-icon.spec.tsx | 26 ++++ .../activity-action-icon.tsx} | 25 ++-- .../ui/activity-action-icon/index.ts | 4 + .../ui/activity-action-picture/index.ts | 4 - .../activity-list-item.spec.tsx.snap | 49 ++++++++ .../activity-list-item.spec.tsx | 23 ++++ .../activity-list-item/activity-list-item.tsx | 13 +- .../skeleton-activity-list-item.spec.tsx.snap | 51 ++++++++ .../skeleton-activity-list-item.spec.tsx | 21 ++++ 14 files changed, 310 insertions(+), 24 deletions(-) create mode 100644 src/entities/activities/ui/activity-action-icon/__snapshots__/activity-action-icon.spec.tsx.snap rename src/entities/activities/ui/{activity-action-picture/activity-action-picture.module.css => activity-action-icon/activity-action-icon.module.css} (100%) rename src/entities/activities/ui/{activity-action-picture/activity-action-picture.module.css.d.ts => activity-action-icon/activity-action-icon.module.css.d.ts} (100%) create mode 100644 src/entities/activities/ui/activity-action-icon/activity-action-icon.spec.tsx rename src/entities/activities/ui/{activity-action-picture/activity-action-picture.tsx => activity-action-icon/activity-action-icon.tsx} (61%) create mode 100644 src/entities/activities/ui/activity-action-icon/index.ts delete mode 100644 src/entities/activities/ui/activity-action-picture/index.ts create mode 100644 src/entities/activities/ui/activity-list-item/__snapshots__/activity-list-item.spec.tsx.snap create mode 100644 src/entities/activities/ui/activity-list-item/activity-list-item.spec.tsx create mode 100644 src/entities/activities/ui/skeleton-activity-list-item/__snapshots__/skeleton-activity-list-item.spec.tsx.snap create mode 100644 src/entities/activities/ui/skeleton-activity-list-item/skeleton-activity-list-item.spec.tsx diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 7acd0f5c..d7414166 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -5,6 +5,6 @@ ], "*.css": [ "prettier --write --ignore-unknown --config ./.prettierrc --ignore-path ./.prettierignore", - "stylelint --fix -c ./configs/styles/.stylelintrc.json" + "stylelint --fix -c ./.stylelintrc.json" ] } diff --git a/.stylelintrc.json b/.stylelintrc.json index a91b6eb4..a942fd62 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -15,7 +15,6 @@ "media-feature-range-notation": "prefix", "plugin/z-index-value-constraint": { - "min": -1, "max": 10000 }, diff --git a/src/entities/activities/ui/activity-action-icon/__snapshots__/activity-action-icon.spec.tsx.snap b/src/entities/activities/ui/activity-action-icon/__snapshots__/activity-action-icon.spec.tsx.snap new file mode 100644 index 00000000..2bb92e0a --- /dev/null +++ b/src/entities/activities/ui/activity-action-icon/__snapshots__/activity-action-icon.spec.tsx.snap @@ -0,0 +1,115 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`src/entities/activities/ui/activity-action-icon > should render icon for \`create\` action > create 1`] = ` +
+ +
+`; + +exports[`src/entities/activities/ui/activity-action-icon > should render icon for \`remove\` action > remove 1`] = ` +
+ +
+`; + +exports[`src/entities/activities/ui/activity-action-icon > should render icon for \`update\` action > update 1`] = ` +
+ +
+`; + +exports[`src/entities/activities/ui/activity-action-icon/activity-action-icon > should render icon for \`create\` action > create 1`] = ` +
+ +
+`; + +exports[`src/entities/activities/ui/activity-action-icon/activity-action-icon > should render icon for \`remove\` action > remove 1`] = ` +
+ +
+`; + +exports[`src/entities/activities/ui/activity-action-icon/activity-action-icon > should render icon for \`update\` action > update 1`] = ` +
+ +
+`; diff --git a/src/entities/activities/ui/activity-action-picture/activity-action-picture.module.css b/src/entities/activities/ui/activity-action-icon/activity-action-icon.module.css similarity index 100% rename from src/entities/activities/ui/activity-action-picture/activity-action-picture.module.css rename to src/entities/activities/ui/activity-action-icon/activity-action-icon.module.css diff --git a/src/entities/activities/ui/activity-action-picture/activity-action-picture.module.css.d.ts b/src/entities/activities/ui/activity-action-icon/activity-action-icon.module.css.d.ts similarity index 100% rename from src/entities/activities/ui/activity-action-picture/activity-action-picture.module.css.d.ts rename to src/entities/activities/ui/activity-action-icon/activity-action-icon.module.css.d.ts diff --git a/src/entities/activities/ui/activity-action-icon/activity-action-icon.spec.tsx b/src/entities/activities/ui/activity-action-icon/activity-action-icon.spec.tsx new file mode 100644 index 00000000..a378d5a2 --- /dev/null +++ b/src/entities/activities/ui/activity-action-icon/activity-action-icon.spec.tsx @@ -0,0 +1,26 @@ +import { describe, expect, test } from 'vitest'; + +import { ActivityActionIcon } from './activity-action-icon'; + +import { RenderResult, render } from '~/test-utils'; + +describe('src/entities/activities/ui/activity-action-icon/activity-action-icon', () => { + let wrapper: RenderResult; + + const createComponent = (action: string) => { + wrapper = render( + + ); + }; + + const getIcon = () => wrapper.container.querySelector('div')!; + + test.each(['create', 'remove', 'update'])( + 'should render icon for `%s` action', + (action) => { + createComponent(action); + + expect(getIcon()).toMatchSnapshot(action); + } + ); +}); diff --git a/src/entities/activities/ui/activity-action-picture/activity-action-picture.tsx b/src/entities/activities/ui/activity-action-icon/activity-action-icon.tsx similarity index 61% rename from src/entities/activities/ui/activity-action-picture/activity-action-picture.tsx rename to src/entities/activities/ui/activity-action-icon/activity-action-icon.tsx index 41601085..8ff10226 100644 --- a/src/entities/activities/ui/activity-action-picture/activity-action-picture.tsx +++ b/src/entities/activities/ui/activity-action-icon/activity-action-icon.tsx @@ -8,13 +8,11 @@ import { useTranslation } from 'react-i18next'; import { CommonProps } from '@/shared/types'; -import { ActivityAction } from '../../model'; +import styles from './activity-action-icon.module.css'; -import styles from './activity-action-picture.module.css'; - -export interface ActivityActionPictureProps - extends CommonProps, - ActivityAction {} +export interface ActivityActionIconProps extends CommonProps { + readonly action: string; +} const colorMap: Record = { create: 'success', @@ -28,19 +26,20 @@ const iconMap: Record = { update: , }; -export const ActivityActionPicture: React.FC = - React.memo((props) => { - const { name, className, } = props; +export const ActivityActionIcon: React.FC = React.memo( + (props) => { + const { action, className, } = props; const { t, } = useTranslation('activities'); - const label = t(`type.${name}`)!; + const label = t(`type.${action}`)!; return ( - {iconMap[name]} + {iconMap[action]} ); - }); + } +); diff --git a/src/entities/activities/ui/activity-action-icon/index.ts b/src/entities/activities/ui/activity-action-icon/index.ts new file mode 100644 index 00000000..9d8229e3 --- /dev/null +++ b/src/entities/activities/ui/activity-action-icon/index.ts @@ -0,0 +1,4 @@ +export { + ActivityActionIcon, + type ActivityActionIconProps +} from './activity-action-icon'; diff --git a/src/entities/activities/ui/activity-action-picture/index.ts b/src/entities/activities/ui/activity-action-picture/index.ts deleted file mode 100644 index 8f7ad465..00000000 --- a/src/entities/activities/ui/activity-action-picture/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - ActivityActionPicture, - type ActivityActionPictureProps -} from './activity-action-picture'; diff --git a/src/entities/activities/ui/activity-list-item/__snapshots__/activity-list-item.spec.tsx.snap b/src/entities/activities/ui/activity-list-item/__snapshots__/activity-list-item.spec.tsx.snap new file mode 100644 index 00000000..78d3ab3b --- /dev/null +++ b/src/entities/activities/ui/activity-list-item/__snapshots__/activity-list-item.spec.tsx.snap @@ -0,0 +1,49 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`src/entities/activities/ui/activity-list-item/activity-list-item > should render activity list item 1`] = ` +
  • +
    +
    + +
    +
    +
    + + card.text + +

    + +

    +
    +
  • +`; diff --git a/src/entities/activities/ui/activity-list-item/activity-list-item.spec.tsx b/src/entities/activities/ui/activity-list-item/activity-list-item.spec.tsx new file mode 100644 index 00000000..37837d31 --- /dev/null +++ b/src/entities/activities/ui/activity-list-item/activity-list-item.spec.tsx @@ -0,0 +1,23 @@ +import { describe, expect, test } from 'vitest'; + +import { ActivityListItem } from './activity-list-item'; + +import { RenderResult, activities, render } from '~/test-utils'; + +describe('src/entities/activities/ui/activity-list-item/activity-list-item', () => { + let wrapper: RenderResult; + + const createComponent = () => { + wrapper = render( + + ); + }; + + const findItem = () => wrapper.getByRole('listitem'); + + test('should render activity list item', () => { + createComponent(); + + expect(findItem()).toMatchSnapshot(); + }); +}); diff --git a/src/entities/activities/ui/activity-list-item/activity-list-item.tsx b/src/entities/activities/ui/activity-list-item/activity-list-item.tsx index 8406fb0f..ebcb95a3 100644 --- a/src/entities/activities/ui/activity-list-item/activity-list-item.tsx +++ b/src/entities/activities/ui/activity-list-item/activity-list-item.tsx @@ -8,19 +8,22 @@ import cn from 'classnames'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { Activity } from '@/shared/api'; +import { ActivityDto } from '@/shared/api'; import { CommonProps } from '@/shared/types'; import { DateTime } from '@/shared/ui'; -import { ActivityActionPicture } from '../activity-action-picture'; +import { ActivityActionIcon } from '../activity-action-icon'; import styles from './activity-list-item.module.css'; export interface ActivityListItemProps extends CommonProps, - Activity, - Omit {} + ActivityDto, + Omit {} +/** + * @todo Rework props. Stay only needed + */ export const ActivityListItem: React.FC = (props) => { const { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -42,7 +45,7 @@ export const ActivityListItem: React.FC = (props) => { return ( - + should render skeleton of activity list item 1`] = ` +
  • +
    + +
    + +
    +
    +
    +
    + + + +

    + +

    +
    +
  • +`; diff --git a/src/entities/activities/ui/skeleton-activity-list-item/skeleton-activity-list-item.spec.tsx b/src/entities/activities/ui/skeleton-activity-list-item/skeleton-activity-list-item.spec.tsx new file mode 100644 index 00000000..0b4c0765 --- /dev/null +++ b/src/entities/activities/ui/skeleton-activity-list-item/skeleton-activity-list-item.spec.tsx @@ -0,0 +1,21 @@ +import { describe, expect, test } from 'vitest'; + +import { SkeletonActivityListItem } from './skeleton-activity-list-item'; + +import { RenderResult, render } from '~/test-utils'; + +describe('src/entities/activities/ui/skeleton-activity-list-item/skeleton-activity-list-item', () => { + let wrapper: RenderResult; + + const createComponent = () => { + wrapper = render(); + }; + + const findItem = () => wrapper.getByRole('listitem'); + + test('should render skeleton of activity list item', () => { + createComponent(); + + expect(findItem()).toMatchSnapshot(); + }); +}); From 788d77e7ff45367e12aed6a99439f629144d0d9a Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Mon, 25 Nov 2024 23:27:32 +0400 Subject: [PATCH 10/71] refactor(shared): add utility type to write funtion in short --- src/shared/types/common.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/shared/types/common.ts b/src/shared/types/common.ts index a53b3e00..1c6929f7 100644 --- a/src/shared/types/common.ts +++ b/src/shared/types/common.ts @@ -1,13 +1,17 @@ import { Effect, Event } from 'effector'; -import { Template, String, Static } from 'runtypes'; +import { Template, String } from 'runtypes'; export const hex = Template`#${String.withConstraint( (code) => code.length === 3 || code.length === 6 )}`; -export type HEX = Static; +export type HEX = `#${string}`; -export type AnyFunction = (...args: any) => any; -export type VoidFunction = () => void; +export type Fn, Result> = ( + ...args: Params +) => Result; + +export type AnyFunction = Fn; +export type VoidFunction = Fn<[], void>; export interface ChainedParams { readonly otherwise?: Event | Effect; From 6a0b562a01a21d13467736d132a59727fc7c1e82 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Mon, 25 Nov 2024 23:28:11 +0400 Subject: [PATCH 11/71] refactor(shared): rename constants to store search params names and popup names --- src/shared/configs/const/routes.ts | 4 ++-- src/shared/lib/create-popup-control-model.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/configs/const/routes.ts b/src/shared/configs/const/routes.ts index d58ba9c7..758f8a72 100644 --- a/src/shared/configs/const/routes.ts +++ b/src/shared/configs/const/routes.ts @@ -1,4 +1,4 @@ -export const getParams = { +export const SEARCH_PARAMS_NAMES = { popup: 'popup', taskStatus: 'task-status', taskId: 'task', @@ -14,7 +14,7 @@ export const getParams = { page: 'p', } as const; -export const popupsMap = { +export const POPUPS_NAMES = { createTask: 'create-task', updateTask: 'update-task', tags: 'tags', diff --git a/src/shared/lib/create-popup-control-model.spec.ts b/src/shared/lib/create-popup-control-model.spec.ts index 1943bdd7..2688ac23 100644 --- a/src/shared/lib/create-popup-control-model.spec.ts +++ b/src/shared/lib/create-popup-control-model.spec.ts @@ -2,7 +2,7 @@ import { allSettled, fork, Scope } from 'effector'; import { createMemoryHistory } from 'history'; import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { getParams, router } from '../configs'; +import { SEARCH_PARAMS_NAMES, router } from '../configs'; import { popupsModel } from '../models'; import { @@ -89,7 +89,7 @@ describe('shared/lib/create-popup-control-model', () => { expect(scope.getState(model.$isOpen)).toBeTruthy(); expect(scope.getState(popupsModel.$popups)).toContain(name); expect(scope.getState(router.$query)).toStrictEqual({ - [getParams.popup]: name, + [SEARCH_PARAMS_NAMES.popup]: name, }); expect(fn).toHaveBeenCalled(); unsubscribe(); From b9c3f0aad78c47463014410974095a48bbbca976 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Tue, 26 Nov 2024 00:12:21 +0400 Subject: [PATCH 12/71] refactor(shared): rewrite friendly list to use it with reatom model --- .../__snapshots__/friendly-list.spec.tsx.snap | 365 ++++++++++++++++++ .../ui/friendly-list/friendly-list.spec.tsx | 140 +++++++ src/shared/ui/friendly-list/friendly-list.tsx | 102 ++--- 3 files changed, 563 insertions(+), 44 deletions(-) create mode 100644 src/shared/ui/friendly-list/__snapshots__/friendly-list.spec.tsx.snap create mode 100644 src/shared/ui/friendly-list/friendly-list.spec.tsx diff --git a/src/shared/ui/friendly-list/__snapshots__/friendly-list.spec.tsx.snap b/src/shared/ui/friendly-list/__snapshots__/friendly-list.spec.tsx.snap new file mode 100644 index 00000000..7c748460 --- /dev/null +++ b/src/shared/ui/friendly-list/__snapshots__/friendly-list.spec.tsx.snap @@ -0,0 +1,365 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`src/shared/ui/friendly-list/friendly-list > should disbale borders if disableBorder=true > disabled borders 1`] = ` +
    +
    +
    +
    +
      +
    • + value1 +
    • +
    • + value2 +
    • +
    • + value3 +
    • +
    • + value4 +
    • +
    +
    +
    +
    +
    +`; + +exports[`src/shared/ui/friendly-list/friendly-list > should map data if it is not array > with getData 1`] = ` +
    +
    +
    +
    +
      +
    • + value1 +
    • +
    • + value2 +
    • +
    • + value3 +
    • +
    • + value4 +
    • +
    +
    +
    +
    +
    +`; + +exports[`src/shared/ui/friendly-list/friendly-list > should render empty phrase if there is no items > emtpy variant 1`] = ` +
    +
    +

    + Empty text +

    +
    +
    +`; + +exports[`src/shared/ui/friendly-list/friendly-list > should render error component if is error > message variant 1`] = ` +
    +
    + + error + +
    +
    +`; + +exports[`src/shared/ui/friendly-list/friendly-list > should render list with items for each element > simple variant 1`] = ` +
    +
    +
    +
    +
      +
    • + value1 +
    • +
    • + value2 +
    • +
    • + value3 +
    • +
    • + value4 +
    • +
    +
    +
    +
    +
    +`; + +exports[`src/shared/ui/friendly-list/friendly-list > should render loading spinner if list is loading > loading variant 1`] = ` +
    +
    +
    +
    +
      +
    • + Skeleton +
    • +
    • + Skeleton +
    • +
    • + Skeleton +
    • +
    • + Skeleton +
    • +
    • + Skeleton +
    • +
    +
    +
    +
    +
    +`; + +exports[`src/shared/ui/friendly-list/friendly-list > slots > should render after slot > after slot 1`] = ` +
    +
    +
    +
    +
      +
    • + value1 +
    • +
    • + value2 +
    • +
    • + value3 +
    • +
    • + value4 +
    • +
    +
    +
    +
    +
    +
    + After slot +
    +
    +
    +`; + +exports[`src/shared/ui/friendly-list/friendly-list > slots > should render before slot > before slot 1`] = ` +
    +
    +
    + Before slot +
    +
    +
    +
    +
    +
      +
    • + value1 +
    • +
    • + value2 +
    • +
    • + value3 +
    • +
    • + value4 +
    • +
    +
    +
    +
    +
    +`; + +exports[`src/shared/ui/friendly-list/friendly-list > slots > should render both slots > both slots 1`] = ` +
    +
    +
    + Before slot +
    +
    +
    +
    +
    +
      +
    • + value1 +
    • +
    • + value2 +
    • +
    • + value3 +
    • +
    • + value4 +
    • +
    +
    +
    +
    +
    +
    + After slot +
    +
    +
    +`; diff --git a/src/shared/ui/friendly-list/friendly-list.spec.tsx b/src/shared/ui/friendly-list/friendly-list.spec.tsx new file mode 100644 index 00000000..210275e1 --- /dev/null +++ b/src/shared/ui/friendly-list/friendly-list.spec.tsx @@ -0,0 +1,140 @@ +import { ListItem } from '@mui/material'; +import { atom } from '@reatom/framework'; +import { beforeEach, describe, expect, test } from 'vitest'; + +import { FriendlyList, FriendlyListProps } from './friendly-list'; + +import { RenderResult, TestCtx, createTestCtx, render } from '~/test-utils'; + +describe('src/shared/ui/friendly-list/friendly-list', () => { + let ctx: TestCtx; + let wrapper: RenderResult; + + const dataAtom = atom( + [ + { + value: 'value1', + }, + { + value: 'value2', + }, + { + value: 'value3', + }, + { + value: 'value4', + } + ], + 'dataAtom' + ); + const pendingAtom = atom(false, 'pendingAtom'); + const errorAtom = atom(null, 'errorAtom'); + + const defaultProps: FriendlyListProps = { + ErrorComponent: ({ error, }) => {error.message}, + ItemComponent: ({ value, ...rest }) => ( + {value} + ), + SkeletonComponent: (props) => Skeleton, + dataAtom, + pendingAtom, + errorAtom, + emptyText: 'Empty text', + getKey: ({ value, }) => value, + skeletonsCount: 5, + className: 'classname', + }; + + const createComponent = ( + props?: Partial> + ) => { + wrapper = render(, { ctx, }); + }; + + const findRoot = () => wrapper.container.querySelector('div')!; + + beforeEach(() => { + ctx = createTestCtx(); + }); + + test('should render list with items for each element', () => { + createComponent(); + + expect(findRoot()).toMatchSnapshot('simple variant'); + }); + + test('should render loading spinner if list is loading', () => { + pendingAtom(ctx, true); + + createComponent(); + + expect(findRoot()).toMatchSnapshot('loading variant'); + }); + + test('should render error component if is error', () => { + errorAtom(ctx, new Error('error')); + + createComponent(); + + expect(findRoot()).toMatchSnapshot('message variant'); + }); + + test('should render empty phrase if there is no items', () => { + dataAtom(ctx, []); + + createComponent(); + + expect(findRoot()).toMatchSnapshot('emtpy variant'); + }); + + test('should map data if it is not array', () => { + dataAtom(ctx, { + items: ctx.get(dataAtom), + }); + + createComponent({ getData: ({ items, }) => items, }); + + expect(findRoot()).toMatchSnapshot('with getData'); + }); + + describe('slots', () => { + test('should render before slot', () => { + createComponent({ + slots: { + before:
    Before slot
    , + }, + }); + + expect(findRoot()).toMatchSnapshot('before slot'); + }); + + test('should render after slot', () => { + createComponent({ + slots: { + after:
    After slot
    , + }, + }); + + expect(findRoot()).toMatchSnapshot('after slot'); + }); + + test('should render both slots', () => { + createComponent({ + slots: { + before:
    Before slot
    , + after:
    After slot
    , + }, + }); + + expect(findRoot()).toMatchSnapshot('both slots'); + }); + }); + + test('should disbale borders if disableBorder=true', () => { + createComponent({ + disableBorder: true, + }); + + expect(findRoot()).toMatchSnapshot('disabled borders'); + }); +}); diff --git a/src/shared/ui/friendly-list/friendly-list.tsx b/src/shared/ui/friendly-list/friendly-list.tsx index a254f241..4e7f4837 100644 --- a/src/shared/ui/friendly-list/friendly-list.tsx +++ b/src/shared/ui/friendly-list/friendly-list.tsx @@ -1,4 +1,3 @@ -import { Query } from '@farfetched/core'; import { List, ListItemProps, @@ -6,9 +5,10 @@ import { PaperProps, Typography } from '@mui/material'; +import { Atom } from '@reatom/framework'; +import { useAtom } from '@reatom/npm-react'; import cn from 'classnames'; -import { useUnit } from 'effector-react'; -import * as React from 'react'; +import { ComponentType, Key, ReactElement, createElement } from 'react'; import { getEmptyArray } from '@/shared/configs'; import { Classes, CommonProps, Slots } from '@/shared/types'; @@ -18,93 +18,107 @@ import { Scrollable } from '../scrollable'; import styles from './friendly-list.module.css'; -interface BaseFriendlyListProps< - Item, - Error, - ListItemOmittedProps = Omit -> extends CommonProps { +interface SkeletonOptions { readonly skeletonsCount: number; - readonly ItemComponent: React.ComponentType< + readonly SkeletonComponent: ComponentType; +} + +interface ErrorOptions { + readonly errorAtom: Atom; + readonly ErrorComponent: ComponentType<{ readonly error: Error }>; +} + +interface LoadingOptions { + readonly pendingAtom: Atom; +} + +interface ItemOptions { + readonly ItemComponent: ComponentType< ListItemOmittedProps & Item & CommonProps >; - readonly SkeletonComponent: React.ComponentType; - readonly ErrorComponent: React.ComponentType<{ readonly error: Error }>; readonly emptyText: string; - readonly getKey: (item: Item) => React.Key | null; + readonly getKey: (item: Item) => Key | null; +} + +interface BaseFriendlyListProps< + Item, + Error, + ListItemOmittedProps = Omit +> extends CommonProps, + SkeletonOptions, + ErrorOptions, + LoadingOptions, + ItemOptions { readonly slots?: Slots<'before' | 'after'>; readonly classes?: Classes<'list'>; readonly disableBorder?: boolean; readonly rootProps?: Omit; } -interface ArrayQueryFriendlyListProps +interface ArrayDataFriendlyListProps extends BaseFriendlyListProps { - readonly $query: Query; + readonly dataAtom: Atom; readonly getData?: never; } -interface AnyQueryFriendlyListProps +interface AnyDataFriendlyListProps extends BaseFriendlyListProps { - readonly $query: Query; + readonly dataAtom: Atom; readonly getData: (data: RawData) => Item[] | null; } export type FriendlyListProps = - | ArrayQueryFriendlyListProps - | AnyQueryFriendlyListProps; + | ArrayDataFriendlyListProps + | AnyDataFriendlyListProps; export const FriendlyList = ( props: FriendlyListProps -): React.ReactElement => { +): ReactElement => { const { - $query, + className, + + dataAtom, + pendingAtom, + errorAtom, + getData, getKey, + emptyText, + ErrorComponent, ItemComponent, SkeletonComponent, + skeletonsCount, - className, + slots, disableBorder, classes, rootProps, } = props; - const finished = useUnit($query.finished); - const [alreadyFetched, setAlreadyFetched] = React.useState(finished); - - React.useEffect(() => { - if (finished) { - setAlreadyFetched(finished); - } - }, [finished]); + const [data] = useAtom(dataAtom); + const [pending] = useAtom(pendingAtom); + const [error] = useAtom(errorAtom); - const query = useUnit($query as Query); - - const arrayData = (getData ? getData(query.data as RawData) : query.data) as + const arrayData = (getData ? getData(data as RawData) : data) as | Item[] | null; const isEmpty = !arrayData?.length; - const isLoading = query.pending && !alreadyFetched; - const isError = !query.error; + const isError = !!error; - let content: React.ReactElement | null = null; + let content: ReactElement | null = null; - if (!isError) { - content = ( -
    - {React.createElement(ErrorComponent, { error: query.error, })} -
    - ); - } else if (isLoading) { + if (isError) { + content =
    {createElement(ErrorComponent, { error, })}
    ; + } else if (pending) { const array = getEmptyArray(skeletonsCount); const count = array.length; const skeletons = array.map((_, index) => - React.createElement(SkeletonComponent, { + createElement(SkeletonComponent, { key: index, divider: index + 1 !== count, } as any) @@ -126,7 +140,7 @@ export const FriendlyList = ( } else { const count = arrayData.length; const items = arrayData.map((item, index) => - React.createElement(ItemComponent, { + createElement(ItemComponent, { ...item, divider: index + 1 !== count, key: getKey(item), From d388fa535dba2f08eeb115700fe6e222bb41891b Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Wed, 27 Nov 2024 00:09:01 +0400 Subject: [PATCH 13/71] refactor(users): rewrite users model --- src/entities/users/lib/index.ts | 2 +- src/entities/users/lib/use-members-model.ts | 11 +++ src/entities/users/lib/useUsersInRoom.ts | 7 -- src/entities/users/model/index.ts | 2 - src/entities/users/model/users-in-room.ts | 40 --------- src/entities/users/models/index.ts | 2 + src/entities/users/models/members/index.ts | 2 + .../users/models/members/model.spec.ts | 87 +++++++++++++++++++ src/entities/users/models/members/model.ts | 66 ++++++++++++++ src/entities/users/models/members/types.ts | 27 ++++++ .../users/{model => models}/search-user.ts | 13 +-- 11 files changed, 204 insertions(+), 55 deletions(-) create mode 100644 src/entities/users/lib/use-members-model.ts delete mode 100644 src/entities/users/lib/useUsersInRoom.ts delete mode 100644 src/entities/users/model/index.ts delete mode 100644 src/entities/users/model/users-in-room.ts create mode 100644 src/entities/users/models/index.ts create mode 100644 src/entities/users/models/members/index.ts create mode 100644 src/entities/users/models/members/model.spec.ts create mode 100644 src/entities/users/models/members/model.ts create mode 100644 src/entities/users/models/members/types.ts rename src/entities/users/{model => models}/search-user.ts (81%) diff --git a/src/entities/users/lib/index.ts b/src/entities/users/lib/index.ts index b5390675..d6811c2c 100644 --- a/src/entities/users/lib/index.ts +++ b/src/entities/users/lib/index.ts @@ -1,2 +1,2 @@ export * from './useSearchedUsers'; -export * from './useUsersInRoom'; +export * from './use-members-model'; diff --git a/src/entities/users/lib/use-members-model.ts b/src/entities/users/lib/use-members-model.ts new file mode 100644 index 00000000..ccc9a79f --- /dev/null +++ b/src/entities/users/lib/use-members-model.ts @@ -0,0 +1,11 @@ +import { MembersModel, membersModel } from '../models'; + +export interface UseMembersModelParams { + readonly roomId: number; +} + +export const useMembersModel = ( + params: UseMembersModelParams +): MembersModel => { + return membersModel.create(params); +}; diff --git a/src/entities/users/lib/useUsersInRoom.ts b/src/entities/users/lib/useUsersInRoom.ts deleted file mode 100644 index 48151d89..00000000 --- a/src/entities/users/lib/useUsersInRoom.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useUnit } from 'effector-react'; - -import { usersInRoomModel } from '../model'; - -export const useUsersInRoom = () => { - return useUnit(usersInRoomModel.query); -}; diff --git a/src/entities/users/model/index.ts b/src/entities/users/model/index.ts deleted file mode 100644 index c288a623..00000000 --- a/src/entities/users/model/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as usersInRoomModel from './users-in-room'; -export * as searchUserModel from './search-user'; diff --git a/src/entities/users/model/users-in-room.ts b/src/entities/users/model/users-in-room.ts deleted file mode 100644 index 6e7f17f8..00000000 --- a/src/entities/users/model/users-in-room.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { cache, createQuery, keepFresh } from '@farfetched/core'; -import { runtypeContract } from '@farfetched/runtypes'; -import { createDomain } from 'effector'; -import { interval } from 'patronum'; -import { Array } from 'runtypes'; - -import { membersApi, user, User } from '@/shared/api'; -import { extractData } from '@/shared/lib'; -import { - getStandardResponse, - InRoomParams, - StandardResponse -} from '@/shared/types'; - -const usersInRoom = createDomain(); - -const handlerFx = usersInRoom.effect(membersApi.getAll); - -export const query = createQuery< - InRoomParams, - StandardResponse, - Error, - StandardResponse, - User[] ->({ - initialData: [], - effect: handlerFx, - contract: runtypeContract(getStandardResponse(Array(user))), - mapData: extractData, -}); - -export const $ids = query.$data.map((users) => users.map((user) => user.id)); -export const $count = query.$data.map((users) => users.length); -export const $hasError = query.$error.map((error) => !!error); - -cache(query); - -keepFresh(query, { - triggers: [interval({ timeout: 5000, })], -}); diff --git a/src/entities/users/models/index.ts b/src/entities/users/models/index.ts new file mode 100644 index 00000000..c801bac2 --- /dev/null +++ b/src/entities/users/models/index.ts @@ -0,0 +1,2 @@ +export * from './members'; +export * as searchUserModel from './search-user'; diff --git a/src/entities/users/models/members/index.ts b/src/entities/users/models/members/index.ts new file mode 100644 index 00000000..85553b78 --- /dev/null +++ b/src/entities/users/models/members/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * as membersModel from './model'; diff --git a/src/entities/users/models/members/model.spec.ts b/src/entities/users/models/members/model.spec.ts new file mode 100644 index 00000000..c12aafb1 --- /dev/null +++ b/src/entities/users/models/members/model.spec.ts @@ -0,0 +1,87 @@ +import { take } from '@reatom/framework'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +import { create } from './model'; +import { MembersModel } from './types'; + +import { + TestCtx, + createTestCtx, + defaultRoom, + handlers, + members, + server +} from '~/test-utils'; + +describe('src/entities/users/models/members/model', () => { + let ctx: TestCtx; + let model: MembersModel; + + const createModel = () => { + model = create({ roomId: defaultRoom.id, }); + }; + + beforeEach(() => { + ctx = createTestCtx(); + + vi.useFakeTimers(); + }); + + afterEach(async () => { + await vi.runOnlyPendingTimersAsync(); + + vi.useRealTimers(); + }); + + test('should create the same model for the same room id', () => { + createModel(); + + const anotherModel = create({ roomId: defaultRoom.id, }); + + expect(anotherModel).toBe(model); + }); + + test('should create different models for different room ids', () => { + createModel(); + + const anotherModel = create({ roomId: 2, }); + + expect(anotherModel).not.toBe(model); + }); + + test('should load members for passed room', async () => { + createModel(); + + const track = ctx.subscribeTrack(model.membersAtom); + + await expect(take(ctx, model.membersAtom)).resolves.toStrictEqual(members); + + track.unsubscribe(); + }); + + test('should load indicate if members is being loaded', async () => { + createModel(); + + const track = ctx.subscribeTrack(model.membersAtom); + + expect(ctx.get(model.pendingAtom)).toBe(true); + + await take(ctx, model.membersAtom); + + expect(ctx.get(model.pendingAtom)).toBe(false); + + track.unsubscribe(); + }); + + test('should store error during loading', async () => { + createModel(); + + server.use(handlers.members.error.members); + + const track = ctx.subscribeTrack(model.membersAtom); + + await expect(take(ctx, model.errorAtom)).resolves.toBeInstanceOf(Error); + + track.unsubscribe(); + }); +}); diff --git a/src/entities/users/models/members/model.ts b/src/entities/users/models/members/model.ts new file mode 100644 index 00000000..5ad8b39d --- /dev/null +++ b/src/entities/users/models/members/model.ts @@ -0,0 +1,66 @@ +import { + mapState, + onDisconnect, + reatomResource, + withCache, + withDataAtom, + withErrorAtom, + withRetry +} from '@reatom/framework'; +import { createMemStorage, reatomPersist } from '@reatom/persist'; + +import { membersApi } from '@/shared/api'; +import { + constructName, + createSingletonFactory, + mapStandardResponse, + retryQuery +} from '@/shared/lib'; + +import { CreateMembersModelParams, MembersModel, Users } from './types'; + +const modelName = 'users'; + +const storage = createMemStorage({ + name: modelName, +}); +// eslint-disable-next-line @reatom/reatom-prefix-rule +const withPersist = reatomPersist(storage); + +export const create = createSingletonFactory( + (params: CreateMembersModelParams): MembersModel => { + const { roomId, } = params; + + const getMembers = reatomResource(async (ctx) => { + return ctx.schedule(() => { + return membersApi.getAll({ roomId, }, ctx.controller.signal); + }); + }, constructName(modelName, roomId.toString(), 'getMembers')).pipe( + withDataAtom([] as Users, mapStandardResponse), + withCache({ withPersist, }), + withRetry(), + withErrorAtom((_ctx, error) => error as Error, { initState: null, }) + ); + + retryQuery({ + query: getMembers, + store: getMembers.dataAtom, + timeout: 5000, + }); + + return { + errorAtom: getMembers.errorAtom, + membersAtom: getMembers.dataAtom, + pendingAtom: getMembers.pendingAtom.pipe( + mapState((_ctx, state) => !!state) + ), + retry: getMembers.retry, + }; + }, + { + key: (params) => constructName(modelName, params.roomId.toString()), + hooks: { + staleOn: (result, staleOn) => onDisconnect(result.membersAtom, staleOn), + }, + } +); diff --git a/src/entities/users/models/members/types.ts b/src/entities/users/models/members/types.ts new file mode 100644 index 00000000..4dce8e44 --- /dev/null +++ b/src/entities/users/models/members/types.ts @@ -0,0 +1,27 @@ +import { Action, Atom } from '@reatom/framework'; +import { Record, String, Static, Number } from 'runtypes'; + +import { StandardResponse } from '@/shared/types'; + +export const userRT = Record({ + id: Number, + email: String, + username: String, + photo: String.nullable(), +}).asReadonly(); + +export interface User extends Static {} +export type Users = User[]; + +export type UserId = User['id']; + +export interface CreateMembersModelParams { + readonly roomId: number; +} + +export interface MembersModel { + readonly membersAtom: Atom; + readonly pendingAtom: Atom; + readonly errorAtom: Atom; + readonly retry: Action<[], Promise>>; +} diff --git a/src/entities/users/model/search-user.ts b/src/entities/users/models/search-user.ts similarity index 81% rename from src/entities/users/model/search-user.ts rename to src/entities/users/models/search-user.ts index c56353a3..4ae2e5bc 100644 --- a/src/entities/users/model/search-user.ts +++ b/src/entities/users/models/search-user.ts @@ -4,24 +4,27 @@ import { createDomain, createEvent, sample } from 'effector'; import { debounce } from 'patronum'; import { Array } from 'runtypes'; -import { SearchUsersQuery, user, User, usersApi } from '@/shared/api'; +import { SearchUsersQuery, user, UserDto, usersApi } from '@/shared/api'; import { extractData } from '@/shared/lib'; import { getStandardResponse, StandardResponse } from '@/shared/types'; +/** + * Move into `features` layer + */ const searchUserDomain = createDomain(); const handlerFx = searchUserDomain.effect< SearchUsersQuery, - StandardResponse, + StandardResponse, Error >(usersApi.searchUsers); export const query = createQuery< SearchUsersQuery, - StandardResponse, + StandardResponse, Error, - StandardResponse, - User[] + StandardResponse, + UserDto[] >({ initialData: [], effect: handlerFx, From 04273b9b192ca29e2a8027f2b6b13ddf10102e55 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Wed, 27 Nov 2024 00:22:36 +0400 Subject: [PATCH 14/71] test(users): add tests for user item components --- .../__snapshots__/ui.spec.tsx.snap | 51 +++++++ .../users/ui/skeleton-user-list-item/index.ts | 5 +- ...list-item.module.css => styles.module.css} | 0 ...module.css.d.ts => styles.module.css.d.ts} | 0 .../ui/skeleton-user-list-item/ui.spec.tsx | 21 +++ .../{skeleton-user-list-item.tsx => ui.tsx} | 2 +- .../__snapshots__/ui.spec.tsx.snap | 140 ++++++++++++++++++ .../users/ui/template-user-list-item/index.ts | 5 +- ...list-item.module.css => styles.module.css} | 0 ...module.css.d.ts => styles.module.css.d.ts} | 0 .../ui/template-user-list-item/ui.spec.tsx | 35 +++++ .../{template-user-list-item.tsx => ui.tsx} | 18 +-- .../__snapshots__/user-avatar.spec.tsx.snap | 15 ++ src/entities/users/ui/user-avatar/index.ts | 2 +- .../users/ui/user-avatar/user-avatar.spec.tsx | 21 +++ .../users/ui/user-avatar/user-avatar.tsx | 3 +- 16 files changed, 294 insertions(+), 24 deletions(-) create mode 100644 src/entities/users/ui/skeleton-user-list-item/__snapshots__/ui.spec.tsx.snap rename src/entities/users/ui/skeleton-user-list-item/{skeleton-user-list-item.module.css => styles.module.css} (100%) rename src/entities/users/ui/skeleton-user-list-item/{skeleton-user-list-item.module.css.d.ts => styles.module.css.d.ts} (100%) create mode 100644 src/entities/users/ui/skeleton-user-list-item/ui.spec.tsx rename src/entities/users/ui/skeleton-user-list-item/{skeleton-user-list-item.tsx => ui.tsx} (93%) create mode 100644 src/entities/users/ui/template-user-list-item/__snapshots__/ui.spec.tsx.snap rename src/entities/users/ui/template-user-list-item/{template-user-list-item.module.css => styles.module.css} (100%) rename src/entities/users/ui/template-user-list-item/{template-user-list-item.module.css.d.ts => styles.module.css.d.ts} (100%) create mode 100644 src/entities/users/ui/template-user-list-item/ui.spec.tsx rename src/entities/users/ui/template-user-list-item/{template-user-list-item.tsx => ui.tsx} (76%) create mode 100644 src/entities/users/ui/user-avatar/__snapshots__/user-avatar.spec.tsx.snap create mode 100644 src/entities/users/ui/user-avatar/user-avatar.spec.tsx diff --git a/src/entities/users/ui/skeleton-user-list-item/__snapshots__/ui.spec.tsx.snap b/src/entities/users/ui/skeleton-user-list-item/__snapshots__/ui.spec.tsx.snap new file mode 100644 index 00000000..9a7b89e5 --- /dev/null +++ b/src/entities/users/ui/skeleton-user-list-item/__snapshots__/ui.spec.tsx.snap @@ -0,0 +1,51 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`src/entities/users/ui/skeleton-user-list-item/ui > should render skeleton list item 1`] = ` +
  • +
    + +
    + +
    +
    +
    +
    +

    + +

    +

    + +

    +
    +
  • +`; diff --git a/src/entities/users/ui/skeleton-user-list-item/index.ts b/src/entities/users/ui/skeleton-user-list-item/index.ts index db5f4ed9..5ecdd1f3 100644 --- a/src/entities/users/ui/skeleton-user-list-item/index.ts +++ b/src/entities/users/ui/skeleton-user-list-item/index.ts @@ -1,4 +1 @@ -export { - SkeletonUserListItem, - type SkeletonUserListItemProps -} from './skeleton-user-list-item'; +export * from './ui'; diff --git a/src/entities/users/ui/skeleton-user-list-item/skeleton-user-list-item.module.css b/src/entities/users/ui/skeleton-user-list-item/styles.module.css similarity index 100% rename from src/entities/users/ui/skeleton-user-list-item/skeleton-user-list-item.module.css rename to src/entities/users/ui/skeleton-user-list-item/styles.module.css diff --git a/src/entities/users/ui/skeleton-user-list-item/skeleton-user-list-item.module.css.d.ts b/src/entities/users/ui/skeleton-user-list-item/styles.module.css.d.ts similarity index 100% rename from src/entities/users/ui/skeleton-user-list-item/skeleton-user-list-item.module.css.d.ts rename to src/entities/users/ui/skeleton-user-list-item/styles.module.css.d.ts diff --git a/src/entities/users/ui/skeleton-user-list-item/ui.spec.tsx b/src/entities/users/ui/skeleton-user-list-item/ui.spec.tsx new file mode 100644 index 00000000..fbb30ecf --- /dev/null +++ b/src/entities/users/ui/skeleton-user-list-item/ui.spec.tsx @@ -0,0 +1,21 @@ +import { describe, expect, test } from 'vitest'; + +import { SkeletonUserListItem } from './ui'; + +import { RenderResult, render } from '~/test-utils'; + +describe('src/entities/users/ui/skeleton-user-list-item/ui', () => { + let wrapper: RenderResult; + + const createComponent = () => { + wrapper = render(); + }; + + const findListItem = () => wrapper.getByRole('listitem'); + + test('should render skeleton list item', () => { + createComponent(); + + expect(findListItem()).toMatchSnapshot(); + }); +}); diff --git a/src/entities/users/ui/skeleton-user-list-item/skeleton-user-list-item.tsx b/src/entities/users/ui/skeleton-user-list-item/ui.tsx similarity index 93% rename from src/entities/users/ui/skeleton-user-list-item/skeleton-user-list-item.tsx rename to src/entities/users/ui/skeleton-user-list-item/ui.tsx index f33f69bd..2f7805c6 100644 --- a/src/entities/users/ui/skeleton-user-list-item/skeleton-user-list-item.tsx +++ b/src/entities/users/ui/skeleton-user-list-item/ui.tsx @@ -11,7 +11,7 @@ import * as React from 'react'; import { CommonProps } from '@/shared/types'; -import styles from './skeleton-user-list-item.module.css'; +import styles from './styles.module.css'; export interface SkeletonUserListItemProps extends CommonProps, ListItemProps {} diff --git a/src/entities/users/ui/template-user-list-item/__snapshots__/ui.spec.tsx.snap b/src/entities/users/ui/template-user-list-item/__snapshots__/ui.spec.tsx.snap new file mode 100644 index 00000000..f6de3c95 --- /dev/null +++ b/src/entities/users/ui/template-user-list-item/__snapshots__/ui.spec.tsx.snap @@ -0,0 +1,140 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`src/entities/users/ui/template-user-list-item/ui > should render actions into right slot > with actions slot 1`] = ` +
  • +
    +
    + +
    +
    +
    +

    + username +

    +

    + email@example.org +

    +
    +
    +
    + actions +
    +
    +
  • +`; + +exports[`src/entities/users/ui/template-user-list-item/ui > should render extra into right slot > with extra slot 1`] = ` +
  • +
    +
    + +
    +
    +
    +

    + username +

    +

    + email@example.org +

    +
    +
    + extra +
    +
  • +`; + +exports[`src/entities/users/ui/template-user-list-item/ui > should render list item for user 1`] = ` +
  • +
    +
    + +
    +
    +
    +

    + username +

    +

    + email@example.org +

    +
    +
  • +`; diff --git a/src/entities/users/ui/template-user-list-item/index.ts b/src/entities/users/ui/template-user-list-item/index.ts index 0bb2c865..5ecdd1f3 100644 --- a/src/entities/users/ui/template-user-list-item/index.ts +++ b/src/entities/users/ui/template-user-list-item/index.ts @@ -1,4 +1 @@ -export { - TemplateUserListItem, - type TemplateUserListItemProps -} from './template-user-list-item'; +export * from './ui'; diff --git a/src/entities/users/ui/template-user-list-item/template-user-list-item.module.css b/src/entities/users/ui/template-user-list-item/styles.module.css similarity index 100% rename from src/entities/users/ui/template-user-list-item/template-user-list-item.module.css rename to src/entities/users/ui/template-user-list-item/styles.module.css diff --git a/src/entities/users/ui/template-user-list-item/template-user-list-item.module.css.d.ts b/src/entities/users/ui/template-user-list-item/styles.module.css.d.ts similarity index 100% rename from src/entities/users/ui/template-user-list-item/template-user-list-item.module.css.d.ts rename to src/entities/users/ui/template-user-list-item/styles.module.css.d.ts diff --git a/src/entities/users/ui/template-user-list-item/ui.spec.tsx b/src/entities/users/ui/template-user-list-item/ui.spec.tsx new file mode 100644 index 00000000..e7a6c65e --- /dev/null +++ b/src/entities/users/ui/template-user-list-item/ui.spec.tsx @@ -0,0 +1,35 @@ +import { describe, expect, test } from 'vitest'; + +import { Slots } from '@/shared/types'; + +import { TemplateUserListItem } from './ui'; + +import { RenderResult, defaultUser, render } from '~/test-utils'; + +describe('src/entities/users/ui/template-user-list-item/ui', () => { + let wrapper: RenderResult; + + const createComponent = (slots?: Slots<'actions' | 'extra'>) => { + wrapper = render(); + }; + + const findListItem = () => wrapper.getByRole('listitem'); + + test('should render list item for user', () => { + createComponent(); + + expect(findListItem()).toMatchSnapshot(); + }); + + test('should render actions into right slot', () => { + createComponent({ actions:
    actions
    , }); + + expect(findListItem()).toMatchSnapshot('with actions slot'); + }); + + test('should render extra into right slot', () => { + createComponent({ extra:
    extra
    , }); + + expect(findListItem()).toMatchSnapshot('with extra slot'); + }); +}); diff --git a/src/entities/users/ui/template-user-list-item/template-user-list-item.tsx b/src/entities/users/ui/template-user-list-item/ui.tsx similarity index 76% rename from src/entities/users/ui/template-user-list-item/template-user-list-item.tsx rename to src/entities/users/ui/template-user-list-item/ui.tsx index 297d9d13..fbce071b 100644 --- a/src/entities/users/ui/template-user-list-item/template-user-list-item.tsx +++ b/src/entities/users/ui/template-user-list-item/ui.tsx @@ -8,32 +8,24 @@ import { import cn from 'classnames'; import * as React from 'react'; -import { User } from '@/shared/api'; +import { UserDto } from '@/shared/api'; import { CommonProps, Slots } from '@/shared/types'; import { UserAvatar } from '../user-avatar'; -import styles from './template-user-list-item.module.css'; +import styles from './styles.module.css'; export interface TemplateUserListItemProps extends CommonProps, - User, - Omit { + Pick, + Omit { readonly slots?: Slots<'actions' | 'extra'>; } export const TemplateUserListItem: React.FC = ( props ) => { - const { - username, - className, - photo, - email, - id: _id, - slots = {}, - ...rest - } = props; + const { username, className, photo, email, slots = {}, ...rest } = props; return ( should render user avatar with passed photo 1`] = ` +
    + Shit-user +
    +`; diff --git a/src/entities/users/ui/user-avatar/index.ts b/src/entities/users/ui/user-avatar/index.ts index 82716fa6..0b55e245 100644 --- a/src/entities/users/ui/user-avatar/index.ts +++ b/src/entities/users/ui/user-avatar/index.ts @@ -1 +1 @@ -export { UserAvatar, type UserAvatarProps } from './user-avatar'; +export * from './user-avatar'; diff --git a/src/entities/users/ui/user-avatar/user-avatar.spec.tsx b/src/entities/users/ui/user-avatar/user-avatar.spec.tsx new file mode 100644 index 00000000..3de72448 --- /dev/null +++ b/src/entities/users/ui/user-avatar/user-avatar.spec.tsx @@ -0,0 +1,21 @@ +import { describe, expect, test } from 'vitest'; + +import { UserAvatar } from './user-avatar'; + +import { RenderResult, render, users } from '~/test-utils'; + +describe('src/entities/users/ui/user-avatar/user-avatar', () => { + let wrapper: RenderResult; + + const createComponent = () => { + wrapper = render(); + }; + + const findAvatar = () => wrapper.container.querySelector('div')!; + + test('should render user avatar with passed photo', () => { + createComponent(); + + expect(findAvatar()).toMatchSnapshot(); + }); +}); diff --git a/src/entities/users/ui/user-avatar/user-avatar.tsx b/src/entities/users/ui/user-avatar/user-avatar.tsx index f7c7be75..d47a9113 100644 --- a/src/entities/users/ui/user-avatar/user-avatar.tsx +++ b/src/entities/users/ui/user-avatar/user-avatar.tsx @@ -2,10 +2,11 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import { Avatar, Tooltip } from '@mui/material'; import * as React from 'react'; -import { User } from '@/shared/api'; import { stringToColor } from '@/shared/lib'; import { CommonProps } from '@/shared/types'; +import { User } from '../../models'; + export interface UserAvatarProps extends CommonProps, Pick { From da8400dee32b00bef4f863b363961b4c11eb6580 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 12 Jan 2025 23:53:52 +0400 Subject: [PATCH 15/71] feat(shared): add reatom form implementation and integrate it with zod --- .eslintignore | 1 + package-lock.json | 74 +++- package.json | 4 +- src/shared/lib/create-rule-from-schema.ts | 3 + src/shared/lib/reatom-form/index.ts | 6 + src/shared/lib/reatom-form/reatom-field.ts | 345 ++++++++++++++++++ src/shared/lib/reatom-form/reatom-form.ts | 326 +++++++++++++++++ src/shared/lib/reatom-form/reatom-zod-form.ts | 45 +++ src/shared/lib/reatom-form/utils.ts | 10 + tsconfig.json | 3 +- vite.config.ts | 7 + vitest.config.ts | 7 + 12 files changed, 818 insertions(+), 13 deletions(-) create mode 100644 .eslintignore create mode 100644 src/shared/lib/reatom-form/index.ts create mode 100644 src/shared/lib/reatom-form/reatom-field.ts create mode 100644 src/shared/lib/reatom-form/reatom-form.ts create mode 100644 src/shared/lib/reatom-form/reatom-zod-form.ts create mode 100644 src/shared/lib/reatom-form/utils.ts diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..e6f28b0b --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +reatom-form diff --git a/package-lock.json b/package-lock.json index fab47a8c..44855fd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@reatom/framework": "^3.4.55", "@reatom/npm-react": "^3.10.2", "@reatom/persist": "^3.4.1", + "@reatom/url": "^3.8.0", "@withease/web-api": "^1.0.1", "atomic-router": "^0.8.0", "atomic-router-react": "^0.8.5", @@ -41,7 +42,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^13.3.1", - "runtypes": "^6.7.0" + "runtypes": "^6.7.0", + "zod": "^3.24.1" }, "devDependencies": { "@babel/cli": "^7.23.0", @@ -3639,6 +3641,17 @@ "@reatom/persist": "^3.3.0" } }, + "node_modules/@reatom/url": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@reatom/url/-/url-3.8.0.tgz", + "integrity": "sha512-BlrF+xdGZ4x2D6LsGQRTG648TeQ+cNyulIh4SbROfDYPDBzgfHEvmAwT3qPWFoP8VR1CNhwujpnSOtUlYIi3hQ==", + "dependencies": { + "@reatom/core": "^3.5.0", + "@reatom/effects": "^3.7.0", + "@reatom/hooks": "^3.4.0", + "@reatom/utils": "^3.4.0" + } + }, "node_modules/@reatom/utils": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/@reatom/utils/-/utils-3.11.0.tgz", @@ -5238,6 +5251,11 @@ "react": "^17 || ^18" } }, + "node_modules/atomic-router/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -9707,6 +9725,12 @@ "node": ">=8" } }, + "node_modules/msw/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true + }, "node_modules/msw/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10123,11 +10147,6 @@ "node": "14 || >=16.14" } }, - "node_modules/path-to-regexp": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -13937,6 +13956,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -16245,6 +16273,17 @@ "@reatom/persist": "^3.3.0" } }, + "@reatom/url": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@reatom/url/-/url-3.8.0.tgz", + "integrity": "sha512-BlrF+xdGZ4x2D6LsGQRTG648TeQ+cNyulIh4SbROfDYPDBzgfHEvmAwT3qPWFoP8VR1CNhwujpnSOtUlYIi3hQ==", + "requires": { + "@reatom/core": "^3.5.0", + "@reatom/effects": "^3.7.0", + "@reatom/hooks": "^3.4.0", + "@reatom/utils": "^3.4.0" + } + }, "@reatom/utils": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/@reatom/utils/-/utils-3.11.0.tgz", @@ -17358,6 +17397,13 @@ "integrity": "sha512-jUNmxqs4zKiTWnvcgNU7u51k+wS8XHYgy7YKTTwiuNB+PPRj+sRAjE+50nyExDygRS5A7yELhT14ziIICjQoqQ==", "requires": { "path-to-regexp": "^6.2.0" + }, + "dependencies": { + "path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" + } } }, "atomic-router-react": { @@ -20558,6 +20604,12 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -20846,11 +20898,6 @@ } } }, - "path-to-regexp": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" - }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -23539,6 +23586,11 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==" } } } diff --git a/package.json b/package.json index b9d7e272..b533f76b 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@reatom/framework": "^3.4.55", "@reatom/npm-react": "^3.10.2", "@reatom/persist": "^3.4.1", + "@reatom/url": "^3.8.0", "@withease/web-api": "^1.0.1", "atomic-router": "^0.8.0", "atomic-router-react": "^0.8.5", @@ -51,7 +52,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^13.3.1", - "runtypes": "^6.7.0" + "runtypes": "^6.7.0", + "zod": "^3.24.1" }, "devDependencies": { "@babel/cli": "^7.23.0", diff --git a/src/shared/lib/create-rule-from-schema.ts b/src/shared/lib/create-rule-from-schema.ts index 87b7715a..06608fce 100644 --- a/src/shared/lib/create-rule-from-schema.ts +++ b/src/shared/lib/create-rule-from-schema.ts @@ -1,6 +1,9 @@ import { Rule } from 'effector-forms'; import Joi from 'joi'; +/** + * @deprecated + */ export const createRuleFromSchema = ( name: string, schema: Joi.Schema diff --git a/src/shared/lib/reatom-form/index.ts b/src/shared/lib/reatom-form/index.ts new file mode 100644 index 00000000..5503f216 --- /dev/null +++ b/src/shared/lib/reatom-form/index.ts @@ -0,0 +1,6 @@ +/** + * @see {@link https://gist.github.com/artalar/d03e4cb93c0af755c6e5e2c77347ef40} + */ +export * from './reatom-field'; +export * from './reatom-form'; +export * from './reatom-zod-form'; diff --git a/src/shared/lib/reatom-form/reatom-field.ts b/src/shared/lib/reatom-form/reatom-field.ts new file mode 100644 index 00000000..2e2a2855 --- /dev/null +++ b/src/shared/lib/reatom-form/reatom-field.ts @@ -0,0 +1,345 @@ +import { + type CtxSpy, + type RecordAtom, + type Action, + type Atom, + type AtomMut, + type Ctx, + __count, + action, + atom, + reatomRecord, + abortCauseContext, + withAbortableSchedule, + isDeepEqual, + noop, + toAbortError, + __thenReatomed, +} from '@reatom/framework'; + +import { toError } from './utils'; + +export interface FieldFocus { + /** The field is focused. */ + active: boolean; + + /** The field state is not equal to the initial state. */ + dirty: boolean; + + /** The field has ever gained and lost focus. */ + touched: boolean; +} + +export interface FieldValidation { + /** The field validation error text. */ + error: undefined | string; + + /** The validation actuality status. */ + triggered: boolean; + + /** The field async validation status */ + validating: boolean; +} + +export interface FocusAtom extends RecordAtom { + /** Action for handling field focus. */ + in: Action<[], void>; + + /** Action for handling field blur. */ + out: Action<[], void>; +} + +export interface ValidationAtom extends RecordAtom { + /** Action to trigger field validation. */ + trigger: Action<[], FieldValidation>; +} + +export interface FieldAtom extends AtomMut { + /** Action for handling field changes, accepts the "value" parameter and applies it to `toState` option. */ + change: Action<[Value], Value>; + + /** Atom of an object with all related focus statuses. */ + focus: FocusAtom; + + /** The initial state of the atom. */ + initState: AtomMut; + + /** Action to reset the state, the value, the validation, and the focus. */ + reset: Action<[], void>; + + /** Atom of an object with all related validation statuses. */ + validation: ValidationAtom; + + /** Atom with the "value" data, computed by the `fromState` option */ + value: Atom; +} + +export type FieldValidateOption = ( + ctx: Ctx, + meta: { + state: State; + value: Value; + focus: FieldFocus; + validation: FieldValidation; + } +) => any; + +export interface FieldOptions { + /** + * The callback to filter "value" changes (from the 'change' action). It should return 'false' to skip the update. + * By default, it always returns `true`. + */ + filter?: (ctx: Ctx, newValue: Value, prevValue: Value) => boolean; + + /** + * The callback to compute the "value" data from the "state" data. + * By default, it returns the "state" data without any transformations. + */ + fromState?: (ctx: CtxSpy, state: State) => Value; + + /** + * The callback used to determine whether the "value" has changed. + * By default, it utilizes `isDeepEqual` from reatom/utils. + */ + isDirty?: (ctx: Ctx, newValue: Value, prevValue: Value) => boolean; + + /** + * The name of the field and all related atoms and actions. + */ + name?: string; + + /** + * The callback to transform the "state" data from the "value" data from the `change` action. + * By default, it returns the "value" data without any transformations. + */ + toState?: (ctx: Ctx, value: Value) => State; + + /** + * The callback to validate the field. + */ + validate?: FieldValidateOption; + + contract?: (sate: State) => any; + + /** + * Defines the reset behavior of the validation state during async validation. + * @default false + */ + keepErrorDuringValidating?: boolean; + + /** + * Defines the reset behavior of the validation state on field change. + * Useful if the validation is triggered on blur or submit only. + * @default !validateOnChange + */ + keepErrorOnChange?: boolean; + + /** + * Defines if the validation should be triggered with every field change. + * @default false + */ + validateOnChange?: boolean; + + /** + * Defines if the validation should be triggered on the field blur. + * @default false + */ + validateOnBlur?: boolean; +} + +export const fieldInitFocus: FieldFocus = { + active: false, + dirty: false, + touched: false, +}; + +export const fieldInitValidation: FieldValidation = { + error: undefined, + triggered: false, + validating: false, +}; + +export const fieldInitValidationLess: FieldValidation = { + error: undefined, + triggered: true, + validating: false, +}; + +export const reatomField = ( + _initState: State, + options: string | FieldOptions = {} +): FieldAtom => { + interface This extends FieldAtom {} + + const { + filter = () => true, + fromState = (ctx, state) => state as unknown as Value, + isDirty = (ctx, newValue, prevValue) => !isDeepEqual(newValue, prevValue), + name = __count(`${typeof _initState}Field`), + toState = (ctx, value) => value as unknown as State, + validate: validateFn, + contract, + validateOnBlur = false, + validateOnChange = false, + keepErrorDuringValidating = false, + keepErrorOnChange = validateOnChange, + } = typeof options === 'string' + ? ({ name: options } as FieldOptions) + : options; + + const initState = atom(_initState, `${name}.initState`); + + const field = atom(_initState, `${name}.field`) as This; + + const value: This['value'] = atom( + (ctx) => fromState(ctx, ctx.spy(field)), + `${name}.value` + ); + + const focus = reatomRecord(fieldInitFocus, `${name}.focus`) as This['focus']; + // @ts-expect-error the original computed state can't be typed properly + focus.__reatom.computer = (ctx, state: FieldFocus) => { + const dirty = isDirty( + ctx, + ctx.spy(value), + fromState(ctx, ctx.spy(initState)) + ); + return state.dirty === dirty ? state : { ...state, dirty }; + }; + + focus.in = action((ctx) => { + focus.merge(ctx, { active: true }); + }, `${name}.focus.in`); + + focus.out = action((ctx) => { + focus.merge(ctx, { active: false, touched: true }); + }, `${name}.focus.out`); + + const validation = reatomRecord( + validateFn || contract ? fieldInitValidation : fieldInitValidationLess, + `${name}.validation` + ) as This['validation']; + if (validateFn || contract) { + // @ts-expect-error the original computed state can't be typed properly + validation.__reatom.computer = (ctx, state: FieldValidation) => { + ctx.spy(value); + return state.triggered ? { ...state, triggered: false } : state; + }; + } + + const validationController = atom( + new AbortController(), + `${name}._validationController` + ); + // prevent collisions for different contexts + validationController.__reatom.initState = () => new AbortController(); + + validation.trigger = action((ctx) => { + const validationValue = ctx.get(validation); + + if (validationValue.triggered) return validationValue; + if (!validateFn && !contract) { + return validation.merge(ctx, { triggered: true }); + } + + ctx.get(validationController).abort(toAbortError('concurrent')); + + const controller = validationController(ctx, new AbortController()); + abortCauseContext.set(ctx.cause, controller); + + const state = ctx.get(field); + const valueValue = ctx.get(value); + const focusValue = ctx.get(focus); + + try { + contract?.(state); + // eslint-disable-next-line no-var + var promise = validateFn?.(withAbortableSchedule(ctx), { + state, + value: valueValue, + focus: focusValue, + validation: validationValue, + }); + } catch (error) { + // eslint-disable-next-line no-var + var message: undefined | string = toError(error); + } + + if (promise instanceof Promise) { + __thenReatomed( + ctx, + promise, + () => { + if (controller.signal.aborted) return; + validation.merge(ctx, { + error: undefined, + triggered: true, + validating: false, + }); + }, + (error) => { + if (controller.signal.aborted) return; + validation.merge(ctx, { + error: toError(error), + triggered: true, + validating: false, + }); + } + ).catch(noop); + + return validation.merge(ctx, { + error: keepErrorDuringValidating ? validationValue.error : undefined, + triggered: true, + validating: true, + }); + } + + return validation.merge(ctx, { + validating: false, + error: message, + triggered: true, + }); + }, `${name}.validation.trigger`); + + const change: This['change'] = action((ctx, newValue) => { + const prevValue = ctx.get(value); + + if (!filter(ctx, newValue, prevValue)) return prevValue; + + field(ctx, toState(ctx, newValue)); + focus.merge(ctx, { touched: true }); + + return ctx.get(value); + }, `${name}.change`); + + const reset: This['reset'] = action((ctx) => { + field(ctx, ctx.get(initState)); + focus(ctx, fieldInitFocus); + validation(ctx, fieldInitValidation); + ctx.get(validationController).abort(toAbortError('reset')); + }, `${name}.reset`); + + if (!keepErrorOnChange) { + field.onChange((ctx) => { + validation(ctx, fieldInitValidation); + ctx.get(validationController).abort(toAbortError('change')); + }); + } + + if (validateOnChange) { + field.onChange((ctx) => validation.trigger(ctx)); + } + + if (validateOnBlur) { + focus.out.onCall((ctx) => validation.trigger(ctx)); + } + + return Object.assign(field, { + change, + focus, + initState, + reset, + validation, + value, + }); +}; diff --git a/src/shared/lib/reatom-form/reatom-form.ts b/src/shared/lib/reatom-form/reatom-form.ts new file mode 100644 index 00000000..300b7803 --- /dev/null +++ b/src/shared/lib/reatom-form/reatom-form.ts @@ -0,0 +1,326 @@ +import { + type ParseAtoms, + type AsyncAction, + withErrorAtom, + withStatusesAtom, + type AsyncStatusesAtom, + type Action, + type Atom, + type Ctx, + type Rec, + type Unsubscribe, + __count, + action, + atom, + isAtom, + reatomAsync, + withAbort, + take, + parseAtoms, + isObject, + isShallowEqual, +} from '@reatom/framework'; + +import { + type FieldAtom, + type FieldFocus, + type FieldValidation, + fieldInitFocus, + fieldInitValidation, + reatomField, + type FieldOptions, +} from './reatom-field'; + +export interface FormFieldOptions + extends FieldOptions { + initState: State; +} + +export type FormInitState = Rec< + | string + | number + | boolean + | null + | undefined + | File + | symbol + | bigint + | Date + | Array + // TODO contract as parsing method + // | ((state: any) => any) + | FieldAtom + | FormFieldOptions + | FormInitState +>; + +export type FormFields = { + [K in keyof T]: T[K] extends FieldAtom + ? T[K] + : T[K] extends Date + ? FieldAtom + : T[K] extends FieldOptions & { initState: infer State } + ? T[K] extends FieldOptions + ? FieldAtom + : T[K] extends FieldOptions + ? FieldAtom + : never + : T[K] extends Rec + ? FormFields + : FieldAtom; +}; + +export type FormState = ParseAtoms< + FormFields +>; + +export type DeepPartial = { + [K in keyof T]?: T[K] extends Rec ? DeepPartial : T[K]; +}; + +export type FormPartialState = + DeepPartial>; + +export interface FieldsAtom extends Atom> { + add: Action<[FieldAtom], Unsubscribe>; + remove: Action<[FieldAtom], void>; +} + +export interface SubmitAction extends AsyncAction<[], void> { + error: Atom; + statusesAtom: AsyncStatusesAtom; +} + +export interface Form { + /** Fields from the init state */ + fields: FormFields; + + fieldsState: Atom>; + + fieldsList: FieldsAtom; + + /** Atom with focus state of the form, computed from all the fields in `fieldsList` */ + focus: Atom; + + init: Action<[initState: FormPartialState], void>; + + /** Action to reset the state, the value, the validation, and the focus states. */ + reset: Action<[], void>; + + /** Submit async handler. It checks the validation of all the fields in `fieldsList`, calls the form's `validate` options handler, and then the `onSubmit` options handler. Check the additional options properties of async action: https://www.reatom.dev/package/async/. */ + submit: SubmitAction; + + submitted: Atom; + + /** Atom with validation state of the form, computed from all the fields in `fieldsList` */ + validation: Atom; +} + +export interface FormOptions { + name?: string; + + /** The callback to process valid form data */ + onSubmit?: (ctx: Ctx, state: FormState) => void | Promise; + + /** Should reset the state after success submit? @default true */ + resetOnSubmit?: boolean; + + /** The callback to validate form fields. */ + validate?: (ctx: Ctx, state: FormState) => any; +} + +const reatomFormFields = ( + initState: T, + name: string +): FormFields => { + const fields = Array.isArray(initState) + ? ([] as FormFields) + : ({} as FormFields); + for (const [key, value] of Object.entries(initState)) { + if (isAtom(value)) { + // @ts-expect-error bad keys type inference + fields[key] = value as FieldAtom; + } else if (isObject(value) && !(value instanceof Date)) { + if ('initState' in value) { + // @ts-expect-error bad keys type inference + fields[key] = reatomField(value.initState, { + name: `${name}.${key}`, + ...(value as FieldOptions), + }); + } else { + // @ts-expect-error bad keys type inference + fields[key] = reatomFormFields(value, `${name}.${key}`); + } + } else { + // @ts-expect-error bad keys type inference + fields[key] = reatomField(value, { + name: `${name}.${key}`, + }); + } + } + return fields; +}; + +const getFieldsList = ( + fields: FormFields, + acc: Array = [] +): Array => { + for (const field of Object.values(fields)) { + if (isAtom(field)) acc.push(field as FieldAtom); + else getFieldsList(field as FormFields, acc); + } + return acc; +}; + +export const reatomForm = ( + initState: T, + options: string | FormOptions = {} +): Form => { + const { + name = __count('form'), + onSubmit, + resetOnSubmit = true, + validate, + } = typeof options === 'string' + ? ({ name: options } as FormOptions) + : options; + + const fields = reatomFormFields(initState, `${name}.fields`); + + const fieldsState = atom( + (ctx) => parseAtoms(ctx, fields), + `${name}.fieldsState` + ); + + const fieldsList = Object.assign( + atom(getFieldsList(fields), `${name}.fieldsList`), + { + add: action((ctx, fieldAtom) => { + fieldsList(ctx, (list) => [...list, fieldAtom]); + return () => { + fieldsList(ctx, (list) => list.filter((v) => v !== fieldAtom)); + }; + }), + remove: action((ctx, fieldAtom) => { + fieldsList(ctx, (list) => list.filter((v) => v !== fieldAtom)); + }), + } + ); + + const focus = atom((ctx, state = fieldInitFocus) => { + const formFocus = { ...fieldInitFocus }; + + for (const field of ctx.spy(fieldsList)) { + const { active, dirty, touched } = ctx.spy(field.focus); + formFocus.active ||= active; + formFocus.dirty ||= dirty; + formFocus.touched ||= touched; + } + + return isShallowEqual(formFocus, state) ? state : formFocus; + }, `${name}.focus`); + + const validation = atom((ctx, state = fieldInitValidation) => { + const formValid = { ...fieldInitValidation }; + + for (const field of ctx.spy(fieldsList)) { + const { triggered, validating, error } = ctx.spy(field.validation); + formValid.triggered &&= triggered; + formValid.validating ||= validating; + formValid.error ||= error; + } + + return isShallowEqual(formValid, state) ? state : formValid; + }, `${name}.validation`); + + const submitted = atom(false, `${name}.submitted`); + + const reset = action((ctx) => { + ctx.get(fieldsList).forEach((fieldAtom) => fieldAtom.reset(ctx)); + submitted(ctx, false); + submit.errorAtom.reset(ctx); + submit.abort(ctx); + }, `${name}.reset`); + + const reinitState = (ctx: Ctx, initState: FormState, fields: FormFields) => { + for (const [key, value] of Object.entries(initState as Rec)) { + if ( + isObject(value) && + !(value instanceof Date) && + key in fields && + !isAtom(fields[key]) + ) { + reinitState( + ctx, + value, + // @ts-expect-error bad keys type inference + fields[key] as FormFields + ); + } else { + fields[key]?.initState(ctx, value); + } + } + }; + + const init = action((ctx, initState: FormState) => { + reinitState(ctx, initState, fields as FormFields); + }, `${name}.init`); + + const submit = reatomAsync(async (ctx) => { + ctx.get(() => { + for (const field of ctx.get(fieldsList)) { + if (!ctx.get(field.validation).triggered) { + field.validation.trigger(ctx); + } + } + }); + + if (ctx.get(validation).validating) { + await take(ctx, validation, (ctx, { validating }, skip) => { + if (validating) return skip; + }); + } + + const error = ctx.get(validation).error; + + if (error) throw new Error(error); + + const state = ctx.get(fieldsState); + + if (validate) { + const promise = validate(ctx, state); + if (promise instanceof Promise) { + await ctx.schedule(() => promise); + } + } + + if (onSubmit) await ctx.schedule(() => onSubmit(ctx, state)); + + submitted(ctx, true); + + if (resetOnSubmit) { + // do not use `reset` action here to not abort the success + ctx.get(fieldsList).forEach((fieldAtom) => fieldAtom.reset(ctx)); + submit.errorAtom.reset(ctx); + submit.statusesAtom.reset(ctx); + submitted(ctx, false); + } + }, `${name}.onSubmit`).pipe( + withStatusesAtom(), + withAbort(), + withErrorAtom(undefined, { resetTrigger: 'onFulfill' }), + (submit) => Object.assign(submit, { error: submit.errorAtom }) + ); + + return { + fields, + fieldsList, + fieldsState, + focus, + init, + reset, + submit, + submitted, + validation, + }; +}; diff --git a/src/shared/lib/reatom-form/reatom-zod-form.ts b/src/shared/lib/reatom-form/reatom-zod-form.ts new file mode 100644 index 00000000..a1343ad5 --- /dev/null +++ b/src/shared/lib/reatom-form/reatom-zod-form.ts @@ -0,0 +1,45 @@ +import { ZodSchema, ZodError } from 'zod'; + +import { + FormOptions, + reatomForm, + Form, + FormInitState, + FormState, +} from './reatom-form'; + +export interface ZodFormOptions< + T extends FormInitState, + Schema extends ZodSchema> +> extends FormOptions { + readonly schema: Schema; +} + +export const reatomZodForm = < + T extends FormInitState, + Schema extends ZodSchema> +>( + initState: T, + options: ZodFormOptions +): Form => { + const { schema, ...rest } = options; + + const form = reatomForm(initState, { + ...rest, + validate: async (ctx, state) => { + return schema.parseAsync(state).catch((error) => { + if (error instanceof ZodError) { + error.issues.forEach((issue) => { + form.fields[issue.path[0] as keyof Schema].validation.merge(ctx, { + error: issue.message, + }); + }); + } + + throw error; + }); + }, + }); + + return form; +}; diff --git a/src/shared/lib/reatom-form/utils.ts b/src/shared/lib/reatom-form/utils.ts new file mode 100644 index 00000000..d8659711 --- /dev/null +++ b/src/shared/lib/reatom-form/utils.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const toError = (thing: unknown) => { + // eslint-disable-next-line no-nested-ternary + return thing instanceof Error + ? thing instanceof z.ZodError + ? thing.issues[0]?.message + : thing.message + : String(thing ?? 'Unknown error'); +}; diff --git a/tsconfig.json b/tsconfig.json index e698715b..8c1f1cec 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,8 @@ "baseUrl": "", "paths": { "@/*": ["./src/*"], - "~/test-utils": ["./test-utils"] + "~/test-utils": ["./test-utils"], + "@reatom/form": ["./src/shared/lib/reatom-form/index.ts"] } }, "include": ["src"] diff --git a/vite.config.ts b/vite.config.ts index 20bcaf1f..1ba23cce 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -173,6 +173,13 @@ export default defineConfig(({ mode }) => { resolve: { alias: { '@': path.resolve(__dirname, 'src'), + '@reatom/form': path.resolve( + __dirname, + 'src', + 'shared', + 'lib', + 'reatom-form' + ), }, }, css: { diff --git a/vitest.config.ts b/vitest.config.ts index fe6bd2e5..68ef197e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -18,6 +18,13 @@ export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, 'src'), + '@reatom/form': path.resolve( + __dirname, + 'src', + 'shared', + 'lib', + 'reatom-form' + ), '~/test-utils': path.resolve(__dirname, 'test-utils'), }, }, From 64025e9d606b2a2f57d3ed9effa196511c353a65 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 12 Jan 2025 23:55:39 +0400 Subject: [PATCH 16/71] refactor(auth): rewrite registration form with reatom and reatom-form --- src/features/auth/registration/index.ts | 2 +- src/features/auth/registration/lib/index.ts | 1 + .../lib/use-registration-model.ts | 7 + src/features/auth/registration/model.ts | 132 ----------- src/features/auth/registration/model/index.ts | 2 + src/features/auth/registration/model/model.ts | 77 +++++++ src/features/auth/registration/model/types.ts | 11 + src/features/auth/registration/ui.tsx | 145 ------------ .../__snapshots__/form.spec.tsx.snap} | 4 +- .../{ui.module.css => ui/form.module.css} | 0 .../form.module.css.d.ts} | 0 .../{ui.spec.tsx => ui/form.spec.tsx} | 88 ++------ src/features/auth/registration/ui/form.tsx | 206 ++++++++++++++++++ src/features/auth/registration/ui/index.ts | 1 + 14 files changed, 331 insertions(+), 345 deletions(-) create mode 100644 src/features/auth/registration/lib/index.ts create mode 100644 src/features/auth/registration/lib/use-registration-model.ts delete mode 100644 src/features/auth/registration/model.ts create mode 100644 src/features/auth/registration/model/index.ts create mode 100644 src/features/auth/registration/model/model.ts create mode 100644 src/features/auth/registration/model/types.ts delete mode 100644 src/features/auth/registration/ui.tsx rename src/features/auth/registration/{__snapshots__/ui.spec.tsx.snap => ui/__snapshots__/form.spec.tsx.snap} (98%) rename src/features/auth/registration/{ui.module.css => ui/form.module.css} (100%) rename src/features/auth/registration/{ui.module.css.d.ts => ui/form.module.css.d.ts} (100%) rename src/features/auth/registration/{ui.spec.tsx => ui/form.spec.tsx} (76%) create mode 100644 src/features/auth/registration/ui/form.tsx create mode 100644 src/features/auth/registration/ui/index.ts diff --git a/src/features/auth/registration/index.ts b/src/features/auth/registration/index.ts index 6fd51ea2..501d9d34 100644 --- a/src/features/auth/registration/index.ts +++ b/src/features/auth/registration/index.ts @@ -1,2 +1,2 @@ -export * as registrationModel from './model'; +export * as registrationModel from './model/model'; export { RegistrationForm } from './ui'; diff --git a/src/features/auth/registration/lib/index.ts b/src/features/auth/registration/lib/index.ts new file mode 100644 index 00000000..a912ea7c --- /dev/null +++ b/src/features/auth/registration/lib/index.ts @@ -0,0 +1 @@ +export * from './use-registration-model'; diff --git a/src/features/auth/registration/lib/use-registration-model.ts b/src/features/auth/registration/lib/use-registration-model.ts new file mode 100644 index 00000000..73878e81 --- /dev/null +++ b/src/features/auth/registration/lib/use-registration-model.ts @@ -0,0 +1,7 @@ +import { useMemo } from 'react'; + +import { registrationModel } from '../model'; + +export const useRegistrationModel = () => { + return useMemo(() => registrationModel.create(), []); +}; diff --git a/src/features/auth/registration/model.ts b/src/features/auth/registration/model.ts deleted file mode 100644 index c1521d92..00000000 --- a/src/features/auth/registration/model.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { createMutation } from '@farfetched/core'; -import { runtypeContract } from '@farfetched/runtypes'; -import { createDomain, sample } from 'effector'; -import { createForm } from 'effector-forms'; -import Joi from 'joi'; -import { splitMap } from 'patronum'; - -import { authApi, RegistrationParams, user, User } from '@/shared/api'; -import { MIN_LENGTH, MAX_SHORT_LENGTH } from '@/shared/configs'; -import { createRuleFromSchema, isHttpErrorCode } from '@/shared/lib'; -import { StandardResponse, getStandardResponse } from '@/shared/types'; - -const registrationDomain = createDomain(); - -const handlerFx = registrationDomain.effect< - RegistrationParams, - StandardResponse ->(authApi.registration); - -export const mutation = createMutation< - RegistrationParams, - StandardResponse, - StandardResponse, - Error ->({ - effect: handlerFx, - contract: runtypeContract(getStandardResponse(user)), -}); - -interface RegistrationFormParams extends RegistrationParams { - readonly repeatPassword: string; -} - -const schemas = Joi.object({ - email: Joi.string() - .min(MIN_LENGTH) - .max(MAX_SHORT_LENGTH) - .email({ tlds: { allow: false, }, }) - .required() - .messages({ - 'string.empty': 'empty', - 'string.email': 'email', - 'string.min': 'min_length', - 'string.max': 'max_length', - }), - username: Joi.string() - .min(MIN_LENGTH) - .max(MAX_SHORT_LENGTH) - .required() - .messages({ - 'string.empty': 'empty', - 'string.min': 'min_length', - 'string.max': 'max_length', - }), - password: Joi.string() - .min(MIN_LENGTH) - .max(MAX_SHORT_LENGTH) - .required() - .messages({ - 'string.empty': 'empty', - 'string.min': 'min_length', - 'string.max': 'max_length', - }), - repeatPassword: Joi.string().required(), -}); - -export const form = createForm({ - fields: { - email: { - init: '', - rules: [createRuleFromSchema('email', schemas.extract('email'))], - }, - username: { - init: '', - rules: [createRuleFromSchema('username', schemas.extract('username'))], - }, - password: { - init: '', - rules: [createRuleFromSchema('password', schemas.extract('password'))], - }, - repeatPassword: { - init: '', - rules: [ - createRuleFromSchema( - 'repeatPassword', - schemas.extract('repeatPassword') - ) - ], - }, - }, -}); - -sample({ - clock: form.formValidated, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - fn: ({ repeatPassword: _, ...rest }) => rest, - target: mutation.start, -}); - -sample({ - clock: mutation.finished.finally, - target: [ - form.fields.password.resetValue, - form.fields.repeatPassword.resetValue - ], -}); - -const errors = splitMap({ - source: mutation.finished.failure, - cases: { - userAlreadyRegistered: ({ error, }) => { - if (isHttpErrorCode(error, 409)) { - return 'exists'; - } - }, - }, -}); - -sample({ - clock: errors.userAlreadyRegistered, - fn: () => false, - target: form.fields.email.$isValid, -}); - -sample({ - clock: errors.userAlreadyRegistered, - fn: (message) => ({ - rule: 'server', - errorText: message, - }), - target: form.fields.email.addError, -}); diff --git a/src/features/auth/registration/model/index.ts b/src/features/auth/registration/model/index.ts new file mode 100644 index 00000000..f8a7339c --- /dev/null +++ b/src/features/auth/registration/model/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * as registrationModel from './model'; diff --git a/src/features/auth/registration/model/model.ts b/src/features/auth/registration/model/model.ts new file mode 100644 index 00000000..a116272b --- /dev/null +++ b/src/features/auth/registration/model/model.ts @@ -0,0 +1,77 @@ +import { reatomZodForm } from '@reatom/form'; +import { atom } from '@reatom/framework'; +import z from 'zod'; + +import { authApi } from '@/shared/api'; +import { MIN_LENGTH, MAX_SHORT_LENGTH } from '@/shared/configs'; +import { constructName, isHttpErrorCode } from '@/shared/lib'; + +import { RegistrationModel } from './types'; + +const schema = z + .object({ + email: z.string().email('email').nonempty('empty'), + username: z + .string() + .min(MIN_LENGTH, 'min_length') + .max(MAX_SHORT_LENGTH, 'max_length') + .nonempty('empty'), + password: z + .string() + .min(MIN_LENGTH, 'min_length') + .max(MAX_SHORT_LENGTH, 'max_length') + .nonempty('empty'), + repeatPassword: z.string(), + }) + .refine((data) => data.password === data.repeatPassword, { + message: 'equal', + path: ['repeatPassword'], + }); + +export const create = (): RegistrationModel => { + const form = reatomZodForm( + { + username: '', + email: '', + password: '', + repeatPassword: '', + }, + { + name: `registration-form`, + resetOnSubmit: false, + schema, + onSubmit: async (ctx, state) => { + try { + await authApi.registration(state); + } catch (error) { + if (isHttpErrorCode(error, 409)) { + form.fields.email.validation.merge(ctx, { error: 'exists', }); + } + + throw error; + } finally { + form.fields.password.reset(ctx); + form.fields.repeatPassword.reset(ctx); + } + }, + } + ); + + const { submit, } = form; + const { statusesAtom, } = submit; + const { email, password, repeatPassword, username, } = form.fields; + + const submittingAtom = atom( + (ctx) => ctx.spy(statusesAtom).isPending, + constructName('registration-form', 'submittingAtom') + ); + + return { + submit, + submittingAtom, + email, + password, + repeatPassword, + username, + }; +}; diff --git a/src/features/auth/registration/model/types.ts b/src/features/auth/registration/model/types.ts new file mode 100644 index 00000000..d2f4e46d --- /dev/null +++ b/src/features/auth/registration/model/types.ts @@ -0,0 +1,11 @@ +import { FieldAtom } from '@reatom/form'; +import { AsyncAction, Atom } from '@reatom/framework'; + +export interface RegistrationModel { + readonly submit: AsyncAction<[], void>; + readonly submittingAtom: Atom; + readonly email: FieldAtom; + readonly username: FieldAtom; + readonly password: FieldAtom; + readonly repeatPassword: FieldAtom; +} diff --git a/src/features/auth/registration/ui.tsx b/src/features/auth/registration/ui.tsx deleted file mode 100644 index 5b509659..00000000 --- a/src/features/auth/registration/ui.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* eslint-disable sonarjs/no-duplicate-string */ -import { Button } from '@mui/material'; -import cn from 'classnames'; -import { useUnit } from 'effector-react'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { MIN_LENGTH, MAX_SHORT_LENGTH } from '@/shared/configs'; -import { usePreventDefault } from '@/shared/lib'; -import { CommonProps } from '@/shared/types'; -import { Field, Form, PasswordField } from '@/shared/ui'; - -import { form, mutation } from './model'; -import styles from './ui.module.css'; - -export type RegistrationFormProps = CommonProps; - -export const RegistrationForm: React.FC = (props) => { - const { className, } = props; - const { t, } = useTranslation('registration'); - const submit = useUnit(form.submit); - const pending = useUnit(mutation.$pending); - const formTitleText = t('registration_form.title'); - const buttonText = t('registration_form.submit'); - - const onSubmit = usePreventDefault(submit); - - return ( -
    - - - - - - - ); -}; - -const Email: React.FC = () => { - const { t, } = useTranslation('registration'); - const email = useUnit(form.fields.email); - const { errorText, } = email; - - const label = t('registration_form.fields.email'); - const error = t(`registration_form.errors.email.${errorText}`, { - min_symbols_count: MIN_LENGTH, - max_symbols_count: MAX_SHORT_LENGTH, - }); - const errorHelperText = email.isValid ? null : error; - - return ( - - ); -}; - -const Username: React.FC = () => { - const { t, } = useTranslation('registration'); - const username = useUnit(form.fields.username); - const { errorText, } = username; - - const label = t('registration_form.fields.username'); - const error = t(`registration_form.errors.username.${errorText}`, { - min_symbols_count: MIN_LENGTH, - max_symbols_count: MAX_SHORT_LENGTH, - }); - const errorHelperText = username.isValid ? null : error; - - return ( - - ); -}; - -const Password: React.FC = () => { - const { t, } = useTranslation('registration'); - const password = useUnit(form.fields.password); - const { errorText, } = password; - - const label = t('registration_form.fields.password'); - const error = t(`registration_form.errors.password.${errorText}`, { - min_symbols_count: MIN_LENGTH, - max_symbols_count: MAX_SHORT_LENGTH, - }); - const errorHelperText = password.isValid ? null : error; - - return ( - - ); -}; - -const RepeatPassword: React.FC = () => { - const { t, } = useTranslation('registration'); - const repeatPassword = useUnit(form.fields.repeatPassword); - const { errorText, } = repeatPassword; - const label = t('registration_form.fields.repeat_password'); - const error = t(`registration_form.errors.repeat_password.${errorText}`, { - min_symbols_count: MIN_LENGTH, - max_symbols_count: MAX_SHORT_LENGTH, - }); - const errorHelperText = repeatPassword.isValid ? null : error; - - return ( - - ); -}; diff --git a/src/features/auth/registration/__snapshots__/ui.spec.tsx.snap b/src/features/auth/registration/ui/__snapshots__/form.spec.tsx.snap similarity index 98% rename from src/features/auth/registration/__snapshots__/ui.spec.tsx.snap rename to src/features/auth/registration/ui/__snapshots__/form.spec.tsx.snap index 18e12f3e..aedb5ef7 100644 --- a/src/features/auth/registration/__snapshots__/ui.spec.tsx.snap +++ b/src/features/auth/registration/ui/__snapshots__/form.spec.tsx.snap @@ -1,9 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`features/auth/registration/ui > should render form, 4 inputs and button 1`] = ` +exports[`features/auth/registration/ui/form.tsx > should render form, 4 inputs and button 1`] = `
    { +describe('features/auth/registration/ui/form.tsx', () => { const values = { email: 'email@example.com', username: 'username', @@ -71,14 +71,14 @@ describe('features/auth/registration/ui', () => { expect(submit).not.toHaveAttribute('disabled', true); - fireEvent.click(submit); + await wrapper.user.click(submit); }); test('should send registration query with data from fields', async () => { const { submit, username, email, password, repeatPassword, } = foundItems(); fillFields({ email, username, password, repeatPassword, }, values); - fireEvent.click(submit); + await wrapper.user.click(submit); await waitFor(() => { expect(email.value).toBe(values.email); @@ -90,7 +90,7 @@ describe('features/auth/registration/ui', () => { describe('validation', () => { describe('username field', () => { - test('empty field', async () => { + test('should show error message if the field is empty', async () => { const { submit, username, email, password, repeatPassword, } = foundItems(); fillFields( @@ -98,7 +98,7 @@ describe('features/auth/registration/ui', () => { { ...values, username: '', } ); - fireEvent.click(submit); + await wrapper.user.click(submit); await waitFor(() => { const element = wrapper.getByText( @@ -109,7 +109,7 @@ describe('features/auth/registration/ui', () => { }); }); - test('too short username', async () => { + test('should show error if the field is too short', async () => { const { submit, username, email, password, repeatPassword, } = foundItems(); fillFields( @@ -117,7 +117,7 @@ describe('features/auth/registration/ui', () => { { ...values, username: '12', } ); - fireEvent.click(submit); + await wrapper.user.click(submit); await waitFor(() => { const element = wrapper.getByText( @@ -128,7 +128,7 @@ describe('features/auth/registration/ui', () => { }); }); - test('too long username', async () => { + test('should show error is the field is too long', async () => { const { submit, username, email, password, repeatPassword, } = foundItems(); fillFields( @@ -140,7 +140,7 @@ describe('features/auth/registration/ui', () => { } ); - fireEvent.click(submit); + await wrapper.user.click(submit); await waitFor(() => { const element = wrapper.getByText( @@ -153,7 +153,7 @@ describe('features/auth/registration/ui', () => { }); describe('email field', () => { - test('empty field', async () => { + test('should show error if the field is empty', async () => { const { submit, username, email, password, repeatPassword, } = foundItems(); fillFields( @@ -161,7 +161,7 @@ describe('features/auth/registration/ui', () => { { ...values, email: '', } ); - fireEvent.click(submit); + await wrapper.user.click(submit); await waitFor(() => { const element = wrapper.getByText( @@ -172,49 +172,7 @@ describe('features/auth/registration/ui', () => { }); }); - test('too short email', async () => { - const { submit, email, username, password, repeatPassword, } = - foundItems(); - fillFields( - { email, username, password, repeatPassword, }, - { ...values, email: 'e@g.c', } - ); - - fireEvent.click(submit); - - await waitFor(() => { - const element = wrapper.getByText( - 'registration_form.errors.email.min_length' - ); - - expect(element).toBeInTheDocument(); - }); - }); - - test('too long email', async () => { - const { submit, email, username, password, repeatPassword, } = - foundItems(); - fillFields( - { email, username, password, repeatPassword, }, - { - ...values, - email: - 'asdfasdfasdasdfasdfasdfasdfasdfasdffasdfasdfeasdfasdf1123@gmail.com', - } - ); - - fireEvent.click(submit); - - await waitFor(() => { - const element = wrapper.getByText( - 'registration_form.errors.email.max_length' - ); - - expect(element).toBeInTheDocument(); - }); - }); - - test('there is not user with this email', async () => { + test('should show error if there is a user with this email', async () => { server.use(handlers.auth.error.registration); const { submit, email, username, password, repeatPassword, } = @@ -224,7 +182,7 @@ describe('features/auth/registration/ui', () => { { ...values, email: 'asd@gmail.com', } ); - fireEvent.click(submit); + await wrapper.user.click(submit); await waitFor(() => { const element = wrapper.getByText( @@ -237,7 +195,7 @@ describe('features/auth/registration/ui', () => { }); describe('password field', () => { - test('empty field', async () => { + test('should show error if the field is empty', async () => { const { submit, email, username, password, repeatPassword, } = foundItems(); fillFields( @@ -245,7 +203,7 @@ describe('features/auth/registration/ui', () => { { ...values, password: '', } ); - fireEvent.click(submit); + await wrapper.user.click(submit); await waitFor(() => { const element = wrapper.getByText( @@ -256,7 +214,7 @@ describe('features/auth/registration/ui', () => { }); }); - test('too short password', async () => { + test('should show error if the field is too short', async () => { const { submit, email, username, password, repeatPassword, } = foundItems(); fillFields( @@ -264,7 +222,7 @@ describe('features/auth/registration/ui', () => { { ...values, password: 'e@g.c', } ); - fireEvent.click(submit); + await wrapper.user.click(submit); await waitFor(() => { const element = wrapper.getByText( @@ -275,7 +233,7 @@ describe('features/auth/registration/ui', () => { }); }); - test('too long password', async () => { + test('should show error if the field is too long', async () => { const { submit, email, username, password, repeatPassword, } = foundItems(); fillFields( @@ -287,7 +245,7 @@ describe('features/auth/registration/ui', () => { } ); - fireEvent.click(submit); + await wrapper.user.click(submit); await waitFor(() => { const element = wrapper.getByText( @@ -299,8 +257,8 @@ describe('features/auth/registration/ui', () => { }); }); - describe.skip('repeat password field', () => { - test('different passwords', async () => { + describe('repeat password field', () => { + test('should show error if the field is not match with `password` one', async () => { const { submit, email, username, password, repeatPassword, } = foundItems(); fillFields( @@ -308,7 +266,7 @@ describe('features/auth/registration/ui', () => { { ...values, repeatPassword: 'another-password', } ); - fireEvent.click(submit); + await wrapper.user.click(submit); await waitFor(() => { const element = wrapper.getByText( diff --git a/src/features/auth/registration/ui/form.tsx b/src/features/auth/registration/ui/form.tsx new file mode 100644 index 00000000..bf179558 --- /dev/null +++ b/src/features/auth/registration/ui/form.tsx @@ -0,0 +1,206 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import { Button } from '@mui/material'; +import { FieldAtom } from '@reatom/form'; +import { useAction, useAtom } from '@reatom/npm-react'; +import cn from 'classnames'; +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { MIN_LENGTH, MAX_SHORT_LENGTH } from '@/shared/configs'; +import { usePreventDefault } from '@/shared/lib'; +import { CommonProps } from '@/shared/types'; +import { Field, Form, PasswordField } from '@/shared/ui'; + +import { useRegistrationModel } from '../lib'; + +import styles from './form.module.css'; + +export interface RegistrationFormProps extends CommonProps {} + +export const RegistrationForm: React.FC = (props) => { + const { className, } = props; + + const model = useRegistrationModel(); + + const submit = useAction(model.submit); + const [pending] = useAtom(model.submittingAtom); + + const { t, } = useTranslation('registration'); + + const formTitleText = t('registration_form.title'); + const buttonText = t('registration_form.submit'); + + const onSubmit = usePreventDefault(submit); + + return ( + + + + + + + + ); +}; + +interface FieldProps { + readonly fieldAtom: FieldAtom; +} + +const Email: React.FC = (props) => { + const { fieldAtom, } = props; + + const { t, } = useTranslation('registration'); + const [email] = useAtom(fieldAtom); + const [errorText] = useAtom( + (ctx) => ctx.spy(fieldAtom.validation).error, + [fieldAtom] + ); + const onChange = useAction(fieldAtom.change); + const onFocus = useAction(fieldAtom.focus.in); + const onBlur = useAction(fieldAtom.focus.out); + + const label = t('registration_form.fields.email'); + const error = t(`registration_form.errors.email.${errorText}`, { + min_symbols_count: MIN_LENGTH, + max_symbols_count: MAX_SHORT_LENGTH, + }); + + const isError = !!errorText; + const errorHelperText = isError ? error : null; + + return ( + + ); +}; + +const Username: React.FC = (props) => { + const { fieldAtom, } = props; + + const { t, } = useTranslation('registration'); + + const [username] = useAtom(fieldAtom); + const [errorText] = useAtom( + (ctx) => ctx.spy(fieldAtom.validation).error, + [fieldAtom] + ); + const onChange = useAction(fieldAtom.change); + const onFocus = useAction(fieldAtom.focus.in); + const onBlur = useAction(fieldAtom.focus.out); + + const label = t('registration_form.fields.username'); + const error = t(`registration_form.errors.username.${errorText}`, { + min_symbols_count: MIN_LENGTH, + max_symbols_count: MAX_SHORT_LENGTH, + }); + + console.log(useAtom(fieldAtom.validation)[0]); + + const isError = !!errorText; + const errorHelperText = isError ? error : null; + + return ( + + ); +}; + +const Password: React.FC = (props) => { + const { fieldAtom, } = props; + + const { t, } = useTranslation('registration'); + + const [password] = useAtom(fieldAtom); + const [errorText] = useAtom( + (ctx) => ctx.spy(fieldAtom.validation).error, + [fieldAtom] + ); + const onChange = useAction(fieldAtom.change); + const onFocus = useAction(fieldAtom.focus.in); + const onBlur = useAction(fieldAtom.focus.out); + + const label = t('registration_form.fields.password'); + const error = t(`registration_form.errors.password.${errorText}`, { + min_symbols_count: MIN_LENGTH, + max_symbols_count: MAX_SHORT_LENGTH, + }); + + const isError = !!errorText; + const errorHelperText = isError ? error : null; + + return ( + + ); +}; + +const RepeatPassword: React.FC = (props) => { + const { fieldAtom, } = props; + + const { t, } = useTranslation('registration'); + + const [repeatPassword] = useAtom(fieldAtom); + const [errorText] = useAtom( + (ctx) => ctx.spy(fieldAtom.validation).error, + [fieldAtom] + ); + const onChange = useAction(fieldAtom.change); + const onFocus = useAction(fieldAtom.focus.in); + const onBlur = useAction(fieldAtom.focus.out); + + const label = t('registration_form.fields.repeat_password'); + const error = t(`registration_form.errors.repeat_password.${errorText}`, { + min_symbols_count: MIN_LENGTH, + max_symbols_count: MAX_SHORT_LENGTH, + }); + + const isError = !!errorText; + const errorHelperText = isError ? error : null; + + return ( + + ); +}; diff --git a/src/features/auth/registration/ui/index.ts b/src/features/auth/registration/ui/index.ts new file mode 100644 index 00000000..698d687b --- /dev/null +++ b/src/features/auth/registration/ui/index.ts @@ -0,0 +1 @@ +export * from './form'; From 5b2d1b13cbf269398a8890e6a72ff2b2f9fb1609 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 00:11:28 +0400 Subject: [PATCH 17/71] refactor(shared): rewrite device info model with reatom. Add hook to easier track if device has a small screen --- src/features/page/change-language/ui.tsx | 3 +- .../adaptive-color-scheme-toggler.tsx | 10 +-- .../rooms/create-room/create-room.tsx | 10 +-- src/features/rooms/create-room/model.ts | 10 +-- .../create-room/open-create-room.spec.tsx | 6 +- src/features/rooms/form/model.ts | 4 +- .../rooms/remove-room/menu-item.spec.tsx | 6 +- src/features/rooms/remove-room/model.ts | 4 +- src/features/rooms/update-room/model.ts | 14 ++-- .../update-room/open-form-menu-item.spec.tsx | 6 +- .../rooms/update-room/update-room.tsx | 9 +-- src/features/tags/create/form.tsx | 8 +- src/features/tags/create/model.ts | 4 +- src/features/tags/create/open-button.spec.tsx | 4 +- src/features/tags/remove/model.ts | 10 +-- src/features/tags/update/model.ts | 11 ++- src/features/tags/update/open-button.spec.tsx | 6 +- src/features/tags/update/ui.spec.tsx | 6 +- src/features/tags/update/ui.tsx | 8 +- .../tasks/create-task/create-task.tsx | 9 +-- src/features/tasks/create-task/model.ts | 11 ++- .../tasks/create-task/open-button.spec.tsx | 6 +- src/features/tasks/remove-task/model.ts | 4 +- .../tasks/tasks-filter/tasks-filters.tsx | 4 +- src/features/tasks/update-task/model.ts | 11 ++- .../tasks/update-task/update-task.tsx | 9 +-- src/pages/room-tasks/model.ts | 28 +++---- src/pages/room-tasks/page.tsx | 12 +-- src/shared/models/device-info/index.ts | 1 + src/shared/models/device-info/model.spec.ts | 61 +++++++--------- src/shared/models/device-info/model.ts | 73 +++++++++---------- .../device-info/use-is-small-screen.spec.ts | 52 +++++++++++++ .../models/device-info/use-is-small-screen.ts | 12 +++ .../ui/filters-popover/filters-popover.tsx | 10 +-- .../create-invitation/create-invitation.tsx | 9 +-- src/widgets/rooms/ui/tabs/tabs.tsx | 10 +-- 36 files changed, 246 insertions(+), 215 deletions(-) create mode 100644 src/shared/models/device-info/use-is-small-screen.spec.ts create mode 100644 src/shared/models/device-info/use-is-small-screen.ts diff --git a/src/features/page/change-language/ui.tsx b/src/features/page/change-language/ui.tsx index 0c2d876b..5ca036d4 100644 --- a/src/features/page/change-language/ui.tsx +++ b/src/features/page/change-language/ui.tsx @@ -3,6 +3,7 @@ import { MenuItem, TextField } from '@mui/material'; +import { useAtom } from '@reatom/npm-react'; import { useUnit } from 'effector-react'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; @@ -20,7 +21,7 @@ export const ChangeLanguage: React.FC = () => { i18nModel.$language, i18nModel.changeLanguage ]); - const isMobile = useUnit(deviceInfoModel.$isMobile); + const [isMobile] = useAtom(deviceInfoModel.isMobileAtom); const [t] = useTranslation('common'); const onChange = (event: React.ChangeEvent) => { diff --git a/src/features/page/color-scheme-toggler/adaptive-color-scheme-toggler.tsx b/src/features/page/color-scheme-toggler/adaptive-color-scheme-toggler.tsx index b4cfdf02..1fe66ab2 100644 --- a/src/features/page/color-scheme-toggler/adaptive-color-scheme-toggler.tsx +++ b/src/features/page/color-scheme-toggler/adaptive-color-scheme-toggler.tsx @@ -1,7 +1,6 @@ -import { useUnit } from 'effector-react'; import * as React from 'react'; -import { deviceInfoModel } from '@/shared/models'; +import { useIsSmallScreen } from '@/shared/models'; import { CommonProps } from '@/shared/types'; import { DesktopColorschemeToggler } from './desktop-color-scheme-toggler'; @@ -12,12 +11,7 @@ export interface AdaptiveColorSchemeTogglerProps extends CommonProps {} export const AdaptiveColorSchemeToggler: React.FC< AdaptiveColorSchemeTogglerProps > = (props) => { - const [isMobile, isTableVertical] = useUnit([ - deviceInfoModel.$isMobile, - deviceInfoModel.$isTabletVertical - ]); - - const isMobileToggler = isMobile || isTableVertical; + const isMobileToggler = useIsSmallScreen(); if (isMobileToggler) { return ; diff --git a/src/features/rooms/create-room/create-room.tsx b/src/features/rooms/create-room/create-room.tsx index 290ce1c6..3e75cc59 100644 --- a/src/features/rooms/create-room/create-room.tsx +++ b/src/features/rooms/create-room/create-room.tsx @@ -3,7 +3,7 @@ import { useUnit } from 'effector-react'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { deviceInfoModel } from '@/shared/models'; +import { useIsSmallScreen } from '@/shared/models'; import { BasePopupProps, CommonProps } from '@/shared/types'; import { FullWidthPopup, MainPopup } from '@/shared/ui'; @@ -17,15 +17,11 @@ export interface CreateRoomProps extends CommonProps, BasePopupProps {} export const CreateRoom: React.FC = (props) => { const { t, } = useTranslation('rooms'); const onClose = useUnit(popupControls.close); - const [isMobile, isVertical] = useUnit([ - deviceInfoModel.$isMobile, - deviceInfoModel.$isTabletVertical - ]); + const isFullscreen = useIsSmallScreen(); + const onClick = useUnit(form.submit); const pending = useUnit(mutation.$pending); - const isFullscreen = isMobile || isVertical; - const Popup = isFullscreen ? FullWidthPopup : MainPopup; const titleText = t('actions.create_room.title'); diff --git a/src/features/rooms/create-room/model.ts b/src/features/rooms/create-room/model.ts index c4dd9d94..0ed56243 100644 --- a/src/features/rooms/create-room/model.ts +++ b/src/features/rooms/create-room/model.ts @@ -4,8 +4,8 @@ import { createDomain, sample } from 'effector'; import { roomsModel } from '@/entities/rooms'; -import { CreateRoomParams, room, Room, roomsApi } from '@/shared/api'; -import { i18n, popupsMap } from '@/shared/configs'; +import { CreateRoomParams, room, RoomDto, roomsApi } from '@/shared/api'; +import { i18n, POPUPS_NAMES } from '@/shared/configs'; import { createPopupControlModel } from '@/shared/lib'; import { notificationsModel } from '@/shared/models'; import { getStandardResponse, StandardResponse } from '@/shared/types'; @@ -18,8 +18,8 @@ const handlerFx = createRoomsDomain.effect(roomsApi.create); export const mutation = createMutation< CreateRoomParams, - StandardResponse, - StandardResponse, + StandardResponse, + StandardResponse, Error >({ effect: handlerFx, @@ -29,7 +29,7 @@ export const mutation = createMutation< export const form = roomFormModel.create(); export const popupControls = createPopupControlModel({ - name: popupsMap.createRoom, + name: POPUPS_NAMES.createRoom, }); const { resetValues, formValidated, } = form; diff --git a/src/features/rooms/create-room/open-create-room.spec.tsx b/src/features/rooms/create-room/open-create-room.spec.tsx index 122e7a31..d188d10d 100644 --- a/src/features/rooms/create-room/open-create-room.spec.tsx +++ b/src/features/rooms/create-room/open-create-room.spec.tsx @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, test } from 'vitest'; -import { popupsMap, router } from '@/shared/configs'; +import { POPUPS_NAMES, router } from '@/shared/configs'; import { popupsModel } from '@/shared/models'; import { OpenCreateRoom } from './open-create-room'; @@ -40,6 +40,8 @@ describe('features/rooms/create-room/open-create-room', () => { await wrapper.user.click(button); - expect(scope.getState(popupsModel.$popups)).toContain(popupsMap.createRoom); + expect(scope.getState(popupsModel.$popups)).toContain( + POPUPS_NAMES.createRoom + ); }); }); diff --git a/src/features/rooms/form/model.ts b/src/features/rooms/form/model.ts index 97692c72..66cc8429 100644 --- a/src/features/rooms/form/model.ts +++ b/src/features/rooms/form/model.ts @@ -1,7 +1,7 @@ import { createForm } from 'effector-forms'; import Joi from 'joi'; -import { Room } from '@/shared/api'; +import { RoomDto } from '@/shared/api'; import { MAX_LONG_LENGTH, MAX_SHORT_LENGTH, @@ -9,7 +9,7 @@ import { } from '@/shared/configs'; import { createRuleFromSchema } from '@/shared/lib'; -export interface RoomFormValues extends Pick {} +export interface RoomFormValues extends Pick {} const schemas = { name: Joi.string().min(MIN_LENGTH).max(MAX_SHORT_LENGTH).required().messages({ diff --git a/src/features/rooms/remove-room/menu-item.spec.tsx b/src/features/rooms/remove-room/menu-item.spec.tsx index 320b427c..06d325a5 100644 --- a/src/features/rooms/remove-room/menu-item.spec.tsx +++ b/src/features/rooms/remove-room/menu-item.spec.tsx @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, test } from 'vitest'; -import { popupsMap } from '@/shared/configs'; +import { POPUPS_NAMES } from '@/shared/configs'; import { popupsModel } from '@/shared/models'; import { RemoveRoomMenuItem } from './menu-item'; @@ -43,6 +43,8 @@ describe('features/rooms/remove-room/remove-item', () => { await wrapper.user.click(menuitem); - expect(scope.getState(popupsModel.$popups)).toContain(popupsMap.removeRoom); + expect(scope.getState(popupsModel.$popups)).toContain( + POPUPS_NAMES.removeRoom + ); }); }); diff --git a/src/features/rooms/remove-room/model.ts b/src/features/rooms/remove-room/model.ts index c1d55c79..06b21904 100644 --- a/src/features/rooms/remove-room/model.ts +++ b/src/features/rooms/remove-room/model.ts @@ -6,7 +6,7 @@ import { Literal } from 'runtypes'; import { roomsModel } from '@/entities/rooms'; import { roomsApi } from '@/shared/api'; -import { i18n, popupsMap } from '@/shared/configs'; +import { i18n, POPUPS_NAMES } from '@/shared/configs'; import { createPopupControlModel } from '@/shared/lib'; import { notificationsModel } from '@/shared/models'; import { @@ -22,7 +22,7 @@ const handlerFx = createEffect, Error>( const $id = createStore(null); export const popupControls = createPopupControlModel({ - name: popupsMap.removeRoom, + name: POPUPS_NAMES.removeRoom, sync: false, }); diff --git a/src/features/rooms/update-room/model.ts b/src/features/rooms/update-room/model.ts index 99c35d2e..593ecd9a 100644 --- a/src/features/rooms/update-room/model.ts +++ b/src/features/rooms/update-room/model.ts @@ -5,8 +5,8 @@ import { and, not } from 'patronum'; import { roomModel, roomsModel } from '@/entities/rooms'; -import { UpdateRoomParams, Room, roomsApi, room } from '@/shared/api'; -import { getParams, i18n, popupsMap } from '@/shared/configs'; +import { UpdateRoomParams, RoomDto, roomsApi, room } from '@/shared/api'; +import { SEARCH_PARAMS_NAMES, i18n, POPUPS_NAMES } from '@/shared/configs'; import { createPopupControlModel, createQueryModel } from '@/shared/lib'; import { notificationsModel } from '@/shared/models'; import { StandardResponse, getStandardResponse } from '@/shared/types'; @@ -17,14 +17,14 @@ const updateRoomDomain = createDomain(); const handlerFx = updateRoomDomain.effect< UpdateRoomParams, - StandardResponse, + StandardResponse, Error >(roomsApi.update); export const mutation = createMutation< UpdateRoomParams, - StandardResponse, - StandardResponse, + StandardResponse, + StandardResponse, Error >({ effect: handlerFx, @@ -33,10 +33,10 @@ export const mutation = createMutation< export const form = roomFormModel.create(); export const popupControls = createPopupControlModel({ - name: popupsMap.updateRoom, + name: POPUPS_NAMES.updateRoom, }); export const roomId = createQueryModel({ - name: getParams.roomId, + name: SEARCH_PARAMS_NAMES.roomId, defaultValue: null, }); export const openPopup = createEvent(); diff --git a/src/features/rooms/update-room/open-form-menu-item.spec.tsx b/src/features/rooms/update-room/open-form-menu-item.spec.tsx index 8d2bfe17..addf0e86 100644 --- a/src/features/rooms/update-room/open-form-menu-item.spec.tsx +++ b/src/features/rooms/update-room/open-form-menu-item.spec.tsx @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, test } from 'vitest'; -import { popupsMap, router } from '@/shared/configs'; +import { POPUPS_NAMES, router } from '@/shared/configs'; import { popupsModel } from '@/shared/models'; import { OpenUpdateRoomFormMenuItem } from './open-form-menu-item'; @@ -46,6 +46,8 @@ describe('features/rooms/update-room/open-form-menu-item', () => { await wrapper.user.click(button); - expect(scope.getState(popupsModel.$popups)).toContain(popupsMap.updateRoom); + expect(scope.getState(popupsModel.$popups)).toContain( + POPUPS_NAMES.updateRoom + ); }); }); diff --git a/src/features/rooms/update-room/update-room.tsx b/src/features/rooms/update-room/update-room.tsx index e8473019..fa370fd7 100644 --- a/src/features/rooms/update-room/update-room.tsx +++ b/src/features/rooms/update-room/update-room.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { roomModel } from '@/entities/rooms'; -import { deviceInfoModel } from '@/shared/models'; +import { useIsSmallScreen } from '@/shared/models'; import { BasePopupProps } from '@/shared/types'; import { FullWidthPopup, MainPopup } from '@/shared/ui'; @@ -18,16 +18,11 @@ export const UpdateRoom: React.FC = (props) => { const { t, } = useTranslation('rooms'); const { pending: loading, } = useUnit(roomModel.query); const onClose = useUnit(popupControls.close); - const [isMobile, isVertical] = useUnit([ - deviceInfoModel.$isMobile, - deviceInfoModel.$isTabletVertical - ]); + const isFullscreen = useIsSmallScreen(); const onClick = useUnit(form.submit); const pending = useUnit(mutation.$pending); - const isFullscreen = isMobile || isVertical; - const Popup = isFullscreen ? FullWidthPopup : MainPopup; const titleText = t('actions.update_room.title'); diff --git a/src/features/tags/create/form.tsx b/src/features/tags/create/form.tsx index d8ef4a2f..03651828 100644 --- a/src/features/tags/create/form.tsx +++ b/src/features/tags/create/form.tsx @@ -3,7 +3,7 @@ import { useUnit } from 'effector-react'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { deviceInfoModel } from '@/shared/models'; +import { useIsSmallScreen } from '@/shared/models'; import { BasePopupProps, CommonProps } from '@/shared/types'; import { FullWidthPopup, MainPopup } from '@/shared/ui'; @@ -20,11 +20,7 @@ export const CreateTag: React.FC = (props) => { const pending = useUnit(mutation.$pending); const onClick = useUnit(form.submit); - const [isMobile, isVertical] = useUnit([ - deviceInfoModel.$isMobile, - deviceInfoModel.$isTabletVertical - ]); - const isFullscreen = isMobile || isVertical; + const isFullscreen = useIsSmallScreen(); const Popup = isFullscreen ? FullWidthPopup : MainPopup; diff --git a/src/features/tags/create/model.ts b/src/features/tags/create/model.ts index 0b69c3ef..eceb95a4 100644 --- a/src/features/tags/create/model.ts +++ b/src/features/tags/create/model.ts @@ -5,7 +5,7 @@ import { createEffect, sample } from 'effector'; import { tagsModel } from '@/entities/tags'; import { CreateTagParams, tag, Tag, tagsApi } from '@/shared/api'; -import { i18n, popupsMap, routes } from '@/shared/configs'; +import { i18n, POPUPS_NAMES, routes } from '@/shared/configs'; import { createPopupControlModel } from '@/shared/lib'; import { notificationsModel } from '@/shared/models'; import { StandardResponse, getStandardResponse } from '@/shared/types'; @@ -13,7 +13,7 @@ import { StandardResponse, getStandardResponse } from '@/shared/types'; import { tagFormModel } from '../form'; export const popupControls = createPopupControlModel({ - name: popupsMap.createTag, + name: POPUPS_NAMES.createTag, }); export const form = tagFormModel.create(); diff --git a/src/features/tags/create/open-button.spec.tsx b/src/features/tags/create/open-button.spec.tsx index 2181994b..2116f589 100644 --- a/src/features/tags/create/open-button.spec.tsx +++ b/src/features/tags/create/open-button.spec.tsx @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, test } from 'vitest'; -import { getParams, popupsMap, router } from '@/shared/configs'; +import { SEARCH_PARAMS_NAMES, POPUPS_NAMES, router } from '@/shared/configs'; import { popupControls } from './model'; import { OpenCreateTagButton } from './open-button'; @@ -47,7 +47,7 @@ describe('features/tags/create/open-button', () => { expect(scope.getState(popupControls.$isOpen)).toBeTruthy(); expect(scope.getState(router.$query)).toStrictEqual( expect.objectContaining({ - [getParams.popup]: popupsMap.createTag, + [SEARCH_PARAMS_NAMES.popup]: POPUPS_NAMES.createTag, }) ); }); diff --git a/src/features/tags/remove/model.ts b/src/features/tags/remove/model.ts index b22cbb8a..c2a43020 100644 --- a/src/features/tags/remove/model.ts +++ b/src/features/tags/remove/model.ts @@ -6,7 +6,7 @@ import { Literal } from 'runtypes'; import { tagsModel } from '@/entities/tags'; import { RemoveTagParams, tagsApi } from '@/shared/api'; -import { i18n, popupsMap, routes } from '@/shared/configs'; +import { i18n, POPUPS_NAMES, routes } from '@/shared/configs'; import { createPopupControlModel } from '@/shared/lib'; import { notificationsModel } from '@/shared/models'; import { StandardResponse, getStandardResponse } from '@/shared/types'; @@ -22,7 +22,7 @@ const handlerFx = removeTagDomain.effect< const $id = createStore(null); export const popupControls = createPopupControlModel({ - name: popupsMap.removeTag, + name: POPUPS_NAMES.removeTag, sync: false, }); @@ -50,8 +50,8 @@ sample({ id: $id, roomId: routes.room.tags.$params.map((params) => params.id), }, - filter: ({ id, roomId }) => !!id && !!roomId, - fn: ({ roomId, id }) => { + filter: ({ id, roomId, }) => !!id && !!roomId, + fn: ({ roomId, id, }) => { return { roomId, id, @@ -73,7 +73,7 @@ sample({ update(tagsModel.query, { on: mutation, by: { - success: ({ query, mutation }) => { + success: ({ query, mutation, }) => { if (!query) { return { result: [], diff --git a/src/features/tags/update/model.ts b/src/features/tags/update/model.ts index b63985fb..6dab120b 100644 --- a/src/features/tags/update/model.ts +++ b/src/features/tags/update/model.ts @@ -6,7 +6,12 @@ import { and, not } from 'patronum'; import { tagsModel, tagModel } from '@/entities/tags'; import { UpdateTagParams, Tag, tagsApi, tag, GetTagParams } from '@/shared/api'; -import { getParams, i18n, popupsMap, routes } from '@/shared/configs'; +import { + SEARCH_PARAMS_NAMES, + i18n, + POPUPS_NAMES, + routes +} from '@/shared/configs'; import { createPopupControlModel, createQueryModel } from '@/shared/lib'; import { notificationsModel } from '@/shared/models'; import { StandardResponse, getStandardResponse } from '@/shared/types'; @@ -26,12 +31,12 @@ export const mutation = createMutation< }); export const popupControls = createPopupControlModel({ - name: popupsMap.updateTag, + name: POPUPS_NAMES.updateTag, }); export const $roomId = routes.room.tags.$params.map((params) => params.id); export const form = tagFormModel.create(); export const tagId = createQueryModel({ - name: getParams.tagId, + name: SEARCH_PARAMS_NAMES.tagId, defaultValue: null, }); diff --git a/src/features/tags/update/open-button.spec.tsx b/src/features/tags/update/open-button.spec.tsx index fd7aa8d1..9bfda5c7 100644 --- a/src/features/tags/update/open-button.spec.tsx +++ b/src/features/tags/update/open-button.spec.tsx @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, test } from 'vitest'; -import { getParams, popupsMap, router } from '@/shared/configs'; +import { SEARCH_PARAMS_NAMES, POPUPS_NAMES, router } from '@/shared/configs'; import { popupControls } from './model'; import { OpenUpdateTagButton } from './open-button'; @@ -48,8 +48,8 @@ describe('featuers/tags/update/open-button', () => { await waitFor(() => { expect(scope.getState(popupControls.$isOpen)).toBeTruthy(); expect(scope.getState(router.$query)).toStrictEqual({ - [getParams.popup]: popupsMap.updateTag, - [getParams.tagId]: tagId.toString(), + [SEARCH_PARAMS_NAMES.popup]: POPUPS_NAMES.updateTag, + [SEARCH_PARAMS_NAMES.tagId]: tagId.toString(), }); }); }); diff --git a/src/features/tags/update/ui.spec.tsx b/src/features/tags/update/ui.spec.tsx index d57f6ad0..348b4646 100644 --- a/src/features/tags/update/ui.spec.tsx +++ b/src/features/tags/update/ui.spec.tsx @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, test } from 'vitest'; import { tagModel, tagsModel } from '@/entities/tags'; -import { getParams, popupsMap, router } from '@/shared/configs'; +import { SEARCH_PARAMS_NAMES, POPUPS_NAMES, router } from '@/shared/configs'; import { deviceInfoModel, notificationsModel } from '@/shared/models'; import { openPopup, popupControls } from './model'; @@ -123,8 +123,8 @@ describe('features/tags/update/ui', () => { expect(scope.getState(popupControls.$isOpen)).toBeFalsy(); expect(scope.getState(router.$query)).not.toContainEqual( expect.objectContaining({ - [getParams.tagId]: tagId, - [getParams.popup]: popupsMap.updateTag, + [SEARCH_PARAMS_NAMES.tagId]: tagId, + [SEARCH_PARAMS_NAMES.popup]: POPUPS_NAMES.updateTag, }) ); }); diff --git a/src/features/tags/update/ui.tsx b/src/features/tags/update/ui.tsx index 16755e20..a8913705 100644 --- a/src/features/tags/update/ui.tsx +++ b/src/features/tags/update/ui.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { tagModel } from '@/entities/tags'; -import { deviceInfoModel } from '@/shared/models'; +import { useIsSmallScreen } from '@/shared/models'; import { BasePopupProps, CommonProps } from '@/shared/types'; import { FullWidthPopup, MainPopup } from '@/shared/ui'; @@ -26,11 +26,7 @@ export const UpdateTag: React.FC> = ( const tag = useUnit(tagModel.query); const isLoading = !tag.data; - const [isMobile, isVertical] = useUnit([ - deviceInfoModel.$isMobile, - deviceInfoModel.$isTabletVertical - ]); - const isFullscreen = isMobile || isVertical; + const isFullscreen = useIsSmallScreen(); const Popup = isFullscreen ? FullWidthPopup : MainPopup; diff --git a/src/features/tasks/create-task/create-task.tsx b/src/features/tasks/create-task/create-task.tsx index b635df09..fc74b90f 100644 --- a/src/features/tasks/create-task/create-task.tsx +++ b/src/features/tasks/create-task/create-task.tsx @@ -3,7 +3,7 @@ import { useUnit } from 'effector-react'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { deviceInfoModel } from '@/shared/models'; +import { useIsSmallScreen } from '@/shared/models'; import { BasePopupProps, CommonProps } from '@/shared/types'; import { FullWidthPopup, MainPopup } from '@/shared/ui'; @@ -17,16 +17,11 @@ export interface CreateTaskProps extends CommonProps, BasePopupProps {} export const CreateTask: React.FC = (props) => { const { t, } = useTranslation('tasks'); const onClose = useUnit(popupControls.close); - const [isMobile, isVertical] = useUnit([ - deviceInfoModel.$isMobile, - deviceInfoModel.$isTabletVertical - ]); + const isFullscreen = useIsSmallScreen(); const onClick = useUnit(form.submit); const pending = useUnit(mutation.$pending); - const isFullscreen = isMobile || isVertical; - const Popup = isFullscreen ? FullWidthPopup : MainPopup; const title = t('actions.create_task.title'); diff --git a/src/features/tasks/create-task/model.ts b/src/features/tasks/create-task/model.ts index e3efe9e3..ed902a96 100644 --- a/src/features/tasks/create-task/model.ts +++ b/src/features/tasks/create-task/model.ts @@ -11,7 +11,12 @@ import { task, TaskStatus } from '@/shared/api'; -import { getParams, i18n, popupsMap, routes } from '@/shared/configs'; +import { + SEARCH_PARAMS_NAMES, + i18n, + POPUPS_NAMES, + routes +} from '@/shared/configs'; import { createPopupControlModel, createQueryModel } from '@/shared/lib'; import { notificationsModel } from '@/shared/models'; import { StandardResponse, getStandardResponse } from '@/shared/types'; @@ -39,11 +44,11 @@ export const mutation = createMutation< export const form = taskFormModel.create(); export const popupControls = createPopupControlModel({ - name: popupsMap.createTask, + name: POPUPS_NAMES.createTask, }); export const status = createQueryModel({ - name: getParams.taskStatus, + name: SEARCH_PARAMS_NAMES.taskStatus, defaultValue: null, }); export const openPopup = createEvent(); diff --git a/src/features/tasks/create-task/open-button.spec.tsx b/src/features/tasks/create-task/open-button.spec.tsx index 55379d2f..206077bf 100644 --- a/src/features/tasks/create-task/open-button.spec.tsx +++ b/src/features/tasks/create-task/open-button.spec.tsx @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, test } from 'vitest'; -import { getParams, popupsMap, router } from '@/shared/configs'; +import { SEARCH_PARAMS_NAMES, POPUPS_NAMES, router } from '@/shared/configs'; import { popupControls } from './model'; import { OpenCreateTaskButton } from './open-button'; @@ -50,8 +50,8 @@ describe('features/tasks/create-task/open-button', () => { await waitFor(() => { expect(scope.getState(popupControls.$isOpen)).toBeTruthy(); expect(scope.getState(router.$query)).toStrictEqual({ - [getParams.popup]: popupsMap.createTask, - [getParams.taskStatus]: columnStatus, + [SEARCH_PARAMS_NAMES.popup]: POPUPS_NAMES.createTask, + [SEARCH_PARAMS_NAMES.taskStatus]: columnStatus, }); }); }); diff --git a/src/features/tasks/remove-task/model.ts b/src/features/tasks/remove-task/model.ts index 977ba5f2..f85264ee 100644 --- a/src/features/tasks/remove-task/model.ts +++ b/src/features/tasks/remove-task/model.ts @@ -6,7 +6,7 @@ import { Literal } from 'runtypes'; import { tasksInRoomModel } from '@/entities/tasks'; import { RemoveTaskParams, tasksApi } from '@/shared/api'; -import { i18n, popupsMap, routes } from '@/shared/configs'; +import { i18n, POPUPS_NAMES, routes } from '@/shared/configs'; import { createPopupControlModel } from '@/shared/lib'; import { notificationsModel } from '@/shared/models'; import { StandardResponse, getStandardResponse } from '@/shared/types'; @@ -22,7 +22,7 @@ const handlerFx = removeTaskDomain.effect< const $id = createStore(null); export const popupControls = createPopupControlModel({ - name: popupsMap.removeTask, + name: POPUPS_NAMES.removeTask, sync: false, }); diff --git a/src/features/tasks/tasks-filter/tasks-filters.tsx b/src/features/tasks/tasks-filter/tasks-filters.tsx index efc139e3..634ad6f4 100644 --- a/src/features/tasks/tasks-filter/tasks-filters.tsx +++ b/src/features/tasks/tasks-filter/tasks-filters.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { TagPicker } from '@/entities/tags'; -import { UsersInRoomPicker } from '@/entities/users'; +import { MembersPicker } from '@/entities/users'; import { usePreventDefault, useToggle } from '@/shared/lib'; import { CommonProps } from '@/shared/types'; @@ -108,7 +108,7 @@ const Users: React.FC = () => { const label = t('actions.tasks_filters.fields.authors'); return ( - ({ - name: getParams.taskId, + name: SEARCH_PARAMS_NAMES.taskId, defaultValue: null, }); export const openPopup = createEvent(); diff --git a/src/features/tasks/update-task/update-task.tsx b/src/features/tasks/update-task/update-task.tsx index 35a6bc7c..4ccddba5 100644 --- a/src/features/tasks/update-task/update-task.tsx +++ b/src/features/tasks/update-task/update-task.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { taskModel } from '@/entities/tasks'; -import { deviceInfoModel } from '@/shared/models'; +import { useIsSmallScreen } from '@/shared/models'; import { BasePopupProps, CommonProps } from '@/shared/types'; import { FullWidthPopup, MainPopup } from '@/shared/ui'; @@ -21,16 +21,11 @@ export const UpdateTask: React.FC = (props) => { const onClose = useUnit(popupControls.close); const { data: task, } = useUnit(taskModel.query); - const [isMobile, isVertical] = useUnit([ - deviceInfoModel.$isMobile, - deviceInfoModel.$isTabletVertical - ]); + const isFullscreen = useIsSmallScreen(); const onClick = useUnit(form.submit); const pending = useUnit(mutation.$pending); - const isFullscreen = isMobile || isVertical; - const Popup = isFullscreen ? FullWidthPopup : MainPopup; const loading = !task; diff --git a/src/pages/room-tasks/model.ts b/src/pages/room-tasks/model.ts index debb0c9a..2666de89 100644 --- a/src/pages/room-tasks/model.ts +++ b/src/pages/room-tasks/model.ts @@ -19,12 +19,12 @@ import { tasksInRoomModel } from '@/entities/tasks'; import { usersInRoomModel } from '@/entities/users'; import { - Activity, + ActivityDto, UpdateTaskParams, activitiesApi, activity } from '@/shared/api'; -import { controls, getParams, routes } from '@/shared/configs'; +import { controls, SEARCH_PARAMS_NAMES, routes } from '@/shared/configs'; import { extractData } from '@/shared/lib'; import { sessionModel } from '@/shared/models'; import { @@ -44,7 +44,7 @@ const { formValidated, reset, fields, } = tasksFiltersModel.form; const activitiesDomain = createDomain(); const handlerFx = activitiesDomain.effect< InRoomParams, - StandardResponse> + StandardResponse> >(({ roomId, }) => activitiesApi.getAll({ roomId, count: 6, by: 'createdAt', type: 'desc', }) ); @@ -52,10 +52,10 @@ const $roomId = authorizedRoute.$params.map((params) => params.id); export const query = createQuery< InRoomParams, - StandardResponse>, + StandardResponse>, Error, - StandardResponse>, - PaginationResponse + StandardResponse>, + PaginationResponse >({ initialData: { items: [], totalCount: 0, limit: 5, }, effect: handlerFx, @@ -76,10 +76,10 @@ const queries = [ const mapQuery = (query: RouteQuery) => { return { - authorIds: query[getParams.userId], - tagIds: query[getParams.userId], - before: query[getParams.before], - after: query[getParams.after], + authorIds: query[SEARCH_PARAMS_NAMES.userId], + tagIds: query[SEARCH_PARAMS_NAMES.userId], + before: query[SEARCH_PARAMS_NAMES.before], + after: query[SEARCH_PARAMS_NAMES.after], }; }; @@ -98,10 +98,10 @@ sample({ querySync({ controls, source: { - [getParams.userId]: fields.authorIds.$value, - [getParams.tagId]: fields.tagIds.$value, - [getParams.after]: fields.after.$value, - [getParams.before]: fields.before.$value, + [SEARCH_PARAMS_NAMES.userId]: fields.authorIds.$value, + [SEARCH_PARAMS_NAMES.tagId]: fields.tagIds.$value, + [SEARCH_PARAMS_NAMES.after]: fields.after.$value, + [SEARCH_PARAMS_NAMES.before]: fields.before.$value, }, clock: [formValidated, reset], route: authorizedRoute, diff --git a/src/pages/room-tasks/page.tsx b/src/pages/room-tasks/page.tsx index 193f13d4..18b89f50 100644 --- a/src/pages/room-tasks/page.tsx +++ b/src/pages/room-tasks/page.tsx @@ -1,3 +1,4 @@ +import { useAtom } from '@reatom/npm-react'; import cn from 'classnames'; import { useUnit } from 'effector-react'; import * as React from 'react'; @@ -14,7 +15,7 @@ import { import { roomModel } from '@/entities/rooms'; -import { popupsMap } from '@/shared/configs'; +import { POPUPS_NAMES } from '@/shared/configs'; import { usePageTitle } from '@/shared/lib'; import { deviceInfoModel } from '@/shared/models'; import { CommonProps } from '@/shared/types'; @@ -24,15 +25,16 @@ import styles from './page.module.css'; import { Tasks, Aside, MobileAside } from './ui'; const popupMap: PopupsProps['popupMap'] = { - [popupsMap.createTask]: CreateTask, - [popupsMap.updateTask]: UpdateTask, - [popupsMap.removeTask]: ConfirmRemoveTask, + [POPUPS_NAMES.createTask]: CreateTask, + [POPUPS_NAMES.updateTask]: UpdateTask, + [POPUPS_NAMES.removeTask]: ConfirmRemoveTask, }; const TasksPage: React.FC = (props) => { const { className, } = props; const { t, } = useTranslation('room-tasks'); - const isDesktopLarge = useUnit(deviceInfoModel.$isDesktopLarge); + + const [isDesktopLarge] = useAtom(deviceInfoModel.isDesktopLargeAtom); const room = useUnit(roomModel.query.$data); const title = t('title'); diff --git a/src/shared/models/device-info/index.ts b/src/shared/models/device-info/index.ts index c7f44aa0..697a3590 100644 --- a/src/shared/models/device-info/index.ts +++ b/src/shared/models/device-info/index.ts @@ -1,2 +1,3 @@ export * as deviceInfoModel from './model'; export * from './types'; +export * from './use-is-small-screen'; diff --git a/src/shared/models/device-info/model.spec.ts b/src/shared/models/device-info/model.spec.ts index f125b2b6..117643b4 100644 --- a/src/shared/models/device-info/model.spec.ts +++ b/src/shared/models/device-info/model.spec.ts @@ -1,39 +1,37 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import { createEffect } from 'effector'; import { beforeEach, describe, expect, test } from 'vitest'; -// eslint-disable-next-line no-restricted-imports -import { started } from '../app'; - import { - $device, - $isDesktopLarge, - $isDesktopSmall, - $isMobile, - $isTabletHorizontal, - $isTabletVertical + deviceAtom, + isDesktopLargeAtom, + isDesktopSmallAtom, + isMobileAtom, + isTabletHorizontalAtom, + isTabletVerticalAtom } from './model'; -import { Scope, allSettled, fork } from '~/test-utils'; +import { TestCtx, createTestCtx } from '~/test-utils'; describe('shared/models/device-info/model', () => { - let scope: Scope; + let ctx: TestCtx; beforeEach(async () => { - scope = fork(); + ctx = createTestCtx(); window.innerWidth = 1920; - - await allSettled(started, { scope, }); }); test('should calculate initialal size on app start', () => { - expect(scope.getState($device)).toBe('desktop-large'); - expect(scope.getState($isMobile)).toBeFalsy(); - expect(scope.getState($isTabletVertical)).toBeFalsy(); - expect(scope.getState($isTabletHorizontal)).toBeFalsy(); - expect(scope.getState($isDesktopSmall)).toBeFalsy(); - expect(scope.getState($isDesktopLarge)).toBeTruthy(); + const track = ctx.subscribeTrack(deviceAtom); + + expect(track.lastInput()).toBe('desktop-large'); + expect(ctx.get(isMobileAtom)).toBeFalsy(); + expect(ctx.get(isTabletVerticalAtom)).toBeFalsy(); + expect(ctx.get(isTabletHorizontalAtom)).toBeFalsy(); + expect(ctx.get(isDesktopSmallAtom)).toBeFalsy(); + expect(ctx.get(isDesktopLargeAtom)).toBeTruthy(); + + track.unsubscribe(); }); test.each([ @@ -57,23 +55,16 @@ describe('shared/models/device-info/model', () => { async ({ size, type, }) => { window.innerWidth = size; - await allSettled( - createEffect(() => { - window.dispatchEvent(new Event('resize')); - }), - { scope, } - ); + window.dispatchEvent(new Event('resize')); - expect(scope.getState($device)).toBe(type); - expect(scope.getState($isMobile)).toBe(type === 'mobile'); - expect(scope.getState($isTabletVertical)).toBe( - type === 'tablet-vertical' - ); - expect(scope.getState($isTabletHorizontal)).toBe( + expect(ctx.get(deviceAtom)).toBe(type); + expect(ctx.get(isMobileAtom)).toBe(type === 'mobile'); + expect(ctx.get(isTabletVerticalAtom)).toBe(type === 'tablet-vertical'); + expect(ctx.get(isTabletHorizontalAtom)).toBe( type === 'tablet-horizontal' ); - expect(scope.getState($isDesktopSmall)).toBe(type === 'desktop-small'); - expect(scope.getState($isDesktopLarge)).toBe(type === 'desktop-large'); + expect(ctx.get(isDesktopSmallAtom)).toBe(type === 'desktop-small'); + expect(ctx.get(isDesktopLargeAtom)).toBe(type === 'desktop-large'); } ); }); diff --git a/src/shared/models/device-info/model.ts b/src/shared/models/device-info/model.ts index a00a3be3..c21e3f92 100644 --- a/src/shared/models/device-info/model.ts +++ b/src/shared/models/device-info/model.ts @@ -1,48 +1,47 @@ -import { createDomain, sample } from 'effector'; - -// eslint-disable-next-line no-restricted-imports -import { started } from '../app'; +import { + atom, + mapState, + onConnect, + readonly, + withInit +} from '@reatom/framework'; import { calculateDevice } from './lib'; import { Devices } from './types'; -const deviceInfoDomain = createDomain(); +// eslint-disable-next-line no-underscore-dangle +const _deviceAtom = atom('desktop-large', '_deviceAtom').pipe( + withInit(() => { + return calculateDevice(); + }) +); + +onConnect(_deviceAtom, (ctx) => { + window.addEventListener( + 'resize', + () => { + _deviceAtom(ctx, calculateDevice()); + }, + { + signal: ctx.controller.signal, + } + ); +}); -export const $device = deviceInfoDomain.store('desktop-large'); +export const deviceAtom = readonly(_deviceAtom); -export const $isMobile = $device.map((device) => device === 'mobile'); -export const $isTabletVertical = $device.map( - (device) => device === 'tablet-vertical' +export const isMobileAtom = deviceAtom.pipe( + mapState((_ctx, device) => device === 'mobile') ); -export const $isTabletHorizontal = $device.map( - (device) => device === 'tablet-horizontal' +export const isTabletVerticalAtom = deviceAtom.pipe( + mapState((_ctx, device) => device === 'tablet-vertical') ); -export const $isDesktopSmall = $device.map( - (device) => device === 'desktop-small' +export const isTabletHorizontalAtom = deviceAtom.pipe( + mapState((_ctx, device) => device === 'tablet-horizontal') ); -export const $isDesktopLarge = $device.map( - (device) => device === 'desktop-large' +export const isDesktopSmallAtom = deviceAtom.pipe( + mapState((_ctx, device) => device === 'desktop-small') ); - -const calculateDeviceFx = deviceInfoDomain.effect( - calculateDevice +export const isDesktopLargeAtom = deviceAtom.pipe( + mapState((_ctx, device) => device === 'desktop-large') ); - -export const subscribeFx = deviceInfoDomain.effect(() => { - window.addEventListener('resize', calculateDeviceFx); - return calculateDeviceFx({}); -}); - -export const unsubscribeFx = deviceInfoDomain.effect(() => - window.removeEventListener('resize', calculateDeviceFx) -); - -sample({ - clock: calculateDeviceFx.doneData, - target: $device, -}); - -sample({ - clock: started, - target: subscribeFx, -}); diff --git a/src/shared/models/device-info/use-is-small-screen.spec.ts b/src/shared/models/device-info/use-is-small-screen.spec.ts new file mode 100644 index 00000000..cf7ca1d0 --- /dev/null +++ b/src/shared/models/device-info/use-is-small-screen.spec.ts @@ -0,0 +1,52 @@ +import { beforeEach, describe, expect, test } from 'vitest'; + +import { + isDesktopLargeAtom, + isMobileAtom, + isTabletVerticalAtom +} from './model'; +import { useIsSmallScreen } from './use-is-small-screen'; + +import { + RenderHookResult, + TestCtx, + createTestCtx, + renderHook +} from '~/test-utils'; + +describe('shared/models/device-info/use-is-small-screen.ts', () => { + let ctx: TestCtx; + let wrapper: RenderHookResult; + + const createHook = () => { + wrapper = renderHook(useIsSmallScreen, { ctx, }); + }; + + beforeEach(() => { + ctx = createTestCtx(); + }); + + test('should return true is device is a mobile', () => { + ctx.mock(isMobileAtom, true); + + createHook(); + + expect(wrapper.result.current).toBeTruthy(); + }); + + test('should return true is device is a tablet in vertical mode', () => { + ctx.mock(isTabletVerticalAtom, true); + + createHook(); + + expect(wrapper.result.current).toBeTruthy(); + }); + + test('should return false if device is not neither a mobile or a tablet', () => { + ctx.mock(isDesktopLargeAtom, true); + + createHook(); + + expect(wrapper.result.current).toBeFalsy(); + }); +}); diff --git a/src/shared/models/device-info/use-is-small-screen.ts b/src/shared/models/device-info/use-is-small-screen.ts new file mode 100644 index 00000000..51392193 --- /dev/null +++ b/src/shared/models/device-info/use-is-small-screen.ts @@ -0,0 +1,12 @@ +import { useAtom } from '@reatom/npm-react'; + +import { isMobileAtom, isTabletVerticalAtom } from './model'; + +export const useIsSmallScreen = () => { + return useAtom((ctx) => { + const isMobile = ctx.spy(isMobileAtom); + const isVerticalTablet = ctx.spy(isTabletVerticalAtom); + + return isMobile || isVerticalTablet; + })[0]; +}; diff --git a/src/shared/ui/filters-popover/filters-popover.tsx b/src/shared/ui/filters-popover/filters-popover.tsx index 713add92..d1be1b12 100644 --- a/src/shared/ui/filters-popover/filters-popover.tsx +++ b/src/shared/ui/filters-popover/filters-popover.tsx @@ -1,8 +1,7 @@ import { Tooltip, IconButton, Popover } from '@mui/material'; -import { useUnit } from 'effector-react'; import * as React from 'react'; -import { deviceInfoModel } from '@/shared/models'; +import { useIsSmallScreen } from '@/shared/models'; import { CommonProps, VoidFunction } from '@/shared/types'; import { FullWidthPopup, FullWidthPopupProps } from '../full-width-popup'; @@ -28,12 +27,7 @@ export const FiltersPopover: React.FC = (props) => { const [ref, setRef] = React.useState(null); const popupId = React.useId(); - const [isMobile, isVertical] = useUnit([ - deviceInfoModel.$isMobile, - deviceInfoModel.$isTabletVertical - ]); - - const isPopup = isMobile || isVertical; + const isPopup = useIsSmallScreen(); const child = React.createElement(children, { isPopup, }); diff --git a/src/widgets/invitations/ui/create-invitation/create-invitation.tsx b/src/widgets/invitations/ui/create-invitation/create-invitation.tsx index c44ec53a..9e67b219 100644 --- a/src/widgets/invitations/ui/create-invitation/create-invitation.tsx +++ b/src/widgets/invitations/ui/create-invitation/create-invitation.tsx @@ -13,7 +13,7 @@ import { import { routes } from '@/shared/configs'; import { useParam } from '@/shared/lib'; -import { deviceInfoModel } from '@/shared/models'; +import { useIsSmallScreen } from '@/shared/models'; import { BasePopupProps, CommonProps } from '@/shared/types'; import { FullWidthPopup, MainPopup } from '@/shared/ui'; @@ -30,12 +30,7 @@ export const CreateInvitation: React.FC = (props) => { const close = useUnit(createInvitationModel.popupControls.close); - const [isVertical, isMobile] = useUnit([ - deviceInfoModel.$isTabletVertical, - deviceInfoModel.$isMobile - ]); - - const fullscreenPopup = isVertical || isMobile; + const fullscreenPopup = useIsSmallScreen(); const translation = t('actions', { returnObjects: true, }) as Record< string, diff --git a/src/widgets/rooms/ui/tabs/tabs.tsx b/src/widgets/rooms/ui/tabs/tabs.tsx index 09fd9850..6ac5c6de 100644 --- a/src/widgets/rooms/ui/tabs/tabs.tsx +++ b/src/widgets/rooms/ui/tabs/tabs.tsx @@ -4,13 +4,12 @@ import ListAltIcon from '@mui/icons-material/ListAlt'; import PeopleIcon from '@mui/icons-material/People'; import { TabContext, TabList } from '@mui/lab'; import { Tab } from '@mui/material'; -import { useUnit } from 'effector-react'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { routes } from '@/shared/configs'; import { useParam } from '@/shared/lib'; -import { deviceInfoModel } from '@/shared/models'; +import { useIsSmallScreen } from '@/shared/models'; import { CommonProps } from '@/shared/types'; export const Tabs: React.FC = React.memo(() => { @@ -20,12 +19,9 @@ export const Tabs: React.FC = React.memo(() => { const tabs = t('tabs', { returnObjects: true, }) as Record; - const [isVertical, isMobile] = useUnit([ - deviceInfoModel.$isTabletVertical, - deviceInfoModel.$isMobile - ]); + const isSmallScreen = useIsSmallScreen(); - const showLabels = !isVertical && !isMobile; + const showLabels = !isSmallScreen; const onChange = React.useCallback( (_evt: unknown, value: string) => { From c9dd9e1d75f82250bffbd86c1d155d031bda0baf Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 00:44:39 +0400 Subject: [PATCH 18/71] refactor(activities): rewrite model for activities filters with reatom --- package-lock.json | 491 +++++++++++++++--- package.json | 5 +- .../activities/lib/use-activities-model.ts | 9 - src/entities/activities/lib/use-activities.ts | 20 + .../models/activities/model.spec.ts | 8 +- .../activities/models/activities/model.ts | 47 +- .../activities/models/activities/types.ts | 10 +- .../activities-actions-picker.tsx | 6 +- .../activity-action-icon.spec.tsx.snap | 57 -- .../activities/activities-filters/filters.tsx | 190 ------- .../activities/activities-filters/index.ts | 4 +- .../activities-filters/lib/index.ts | 1 + .../lib/use-activity-filters.ts | 22 + .../activities/activities-filters/model.ts | 28 - .../activities-filters/model/index.ts | 2 + .../activities-filters/model/model.ts | 70 +++ .../activities-filters/model/types.ts | 29 ++ .../__snapshots__/filters.spec.tsx.snap | 78 +-- .../{ => ui}/filters.module.css | 2 +- .../{ => ui}/filters.module.css.d.ts | 0 .../{ => ui}/filters.spec.tsx | 61 ++- .../activities-filters/ui/filters.tsx | 266 ++++++++++ .../activities/activities-filters/ui/index.ts | 2 + 23 files changed, 951 insertions(+), 457 deletions(-) delete mode 100644 src/entities/activities/lib/use-activities-model.ts create mode 100644 src/entities/activities/lib/use-activities.ts delete mode 100644 src/features/activities/activities-filters/filters.tsx create mode 100644 src/features/activities/activities-filters/lib/index.ts create mode 100644 src/features/activities/activities-filters/lib/use-activity-filters.ts delete mode 100644 src/features/activities/activities-filters/model.ts create mode 100644 src/features/activities/activities-filters/model/index.ts create mode 100644 src/features/activities/activities-filters/model/model.ts create mode 100644 src/features/activities/activities-filters/model/types.ts rename src/features/activities/activities-filters/{ => ui}/__snapshots__/filters.spec.tsx.snap (86%) rename src/features/activities/activities-filters/{ => ui}/filters.module.css (100%) rename src/features/activities/activities-filters/{ => ui}/filters.module.css.d.ts (100%) rename src/features/activities/activities-filters/{ => ui}/filters.spec.tsx (76%) create mode 100644 src/features/activities/activities-filters/ui/filters.tsx create mode 100644 src/features/activities/activities-filters/ui/index.ts diff --git a/package-lock.json b/package-lock.json index 44855fd5..34ead715 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "joi": "^17.11.0", "ky": "^1.1.0", "patronum": "^1.20.0", + "qs": "^6.14.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^13.3.1", @@ -59,6 +60,7 @@ "@testing-library/user-event": "^14.5.2", "@types/compose-function": "^0.0.32", "@types/node": "^20.8.7", + "@types/qs": "^6.9.18", "@types/react": "^18.2.31", "@types/react-dom": "^18.2.14", "@typescript-eslint/eslint-plugin": "^6.8.0", @@ -78,6 +80,7 @@ "lint-staged": "^15.0.2", "mock-match-media": "^0.4.3", "msw": "^2.3.1", + "prettier": "^3.4.2", "stylelint": "^16.9.0", "stylelint-color-format": "^1.1.0", "stylelint-config-clean-order": "^6.1.0", @@ -4328,6 +4331,13 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "18.2.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", @@ -5447,6 +5457,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -6188,6 +6227,20 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6405,6 +6458,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -7095,6 +7178,22 @@ "eslint": "7 || 8" } }, + "node_modules/eslint-plugin-effector/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/eslint-plugin-import": { "version": "2.28.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", @@ -7711,9 +7810,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -7770,15 +7873,24 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7790,6 +7902,19 @@ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "dev": true }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -7948,12 +8073,12 @@ "dev": true }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8033,10 +8158,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8059,6 +8184,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/headers-polyfill": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", @@ -9441,6 +9578,15 @@ "semver": "bin/semver" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -9864,10 +10010,13 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true, + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10429,15 +10578,16 @@ } }, "node_modules/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, + "license": "MIT", "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -10532,6 +10682,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -11074,14 +11239,72 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -16746,6 +16969,12 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, + "@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true + }, "@types/react": { "version": "18.2.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", @@ -17545,6 +17774,24 @@ "get-intrinsic": "^1.0.2" } }, + "call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -18102,6 +18349,16 @@ "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "dev": true }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -18250,6 +18507,24 @@ "which-typed-array": "^1.1.10" } }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, "es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -18728,6 +19003,14 @@ "dev": true, "requires": { "prettier": "^2.3.2" + }, + "dependencies": { + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true + } } }, "eslint-plugin-import": { @@ -19136,9 +19419,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { "version": "1.1.5", @@ -19177,15 +19460,20 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, "get-own-enumerable-property-symbols": { @@ -19194,6 +19482,15 @@ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "dev": true }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -19308,13 +19605,9 @@ "dev": true }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "graceful-fs": { "version": "4.2.11", @@ -19369,10 +19662,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { "version": "1.0.0", @@ -19383,6 +19675,14 @@ "has-symbols": "^1.0.2" } }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "headers-polyfill": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", @@ -20408,6 +20708,11 @@ } } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -20694,10 +20999,9 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==" }, "object-keys": { "version": "1.1.1", @@ -21071,9 +21375,9 @@ "dev": true }, "prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true }, "pretty-bytes": { @@ -21151,6 +21455,14 @@ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, + "qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "requires": { + "side-channel": "^1.1.0" + } + }, "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -21540,14 +21852,47 @@ "dev": true }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, "siginfo": { diff --git a/package.json b/package.json index b533f76b..1ab7af4b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test:e2e:ui": "npx playwright test --ui", "lint": "eslint --ext .tsx,.ts src", "lint:fix": "eslint --ext .tsx,.ts src --fix", - "prettier": "prettier --write --config ./.prettierrc src", + "format": "prettier --write --config ./.prettierrc src", "prepare": "husky install", "gen:css-types": "tcm src" }, @@ -49,6 +49,7 @@ "joi": "^17.11.0", "ky": "^1.1.0", "patronum": "^1.20.0", + "qs": "^6.14.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^13.3.1", @@ -69,6 +70,7 @@ "@testing-library/user-event": "^14.5.2", "@types/compose-function": "^0.0.32", "@types/node": "^20.8.7", + "@types/qs": "^6.9.18", "@types/react": "^18.2.31", "@types/react-dom": "^18.2.14", "@typescript-eslint/eslint-plugin": "^6.8.0", @@ -88,6 +90,7 @@ "lint-staged": "^15.0.2", "mock-match-media": "^0.4.3", "msw": "^2.3.1", + "prettier": "^3.4.2", "stylelint": "^16.9.0", "stylelint-color-format": "^1.1.0", "stylelint-config-clean-order": "^6.1.0", diff --git a/src/entities/activities/lib/use-activities-model.ts b/src/entities/activities/lib/use-activities-model.ts deleted file mode 100644 index 8bd41a5b..00000000 --- a/src/entities/activities/lib/use-activities-model.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useMemo } from 'react'; - -import { ActivitiesModel, activititesModel } from '../models'; - -export const useActivitiesModel = (roomId: number): ActivitiesModel => { - return useMemo(() => { - return activititesModel.create({ roomId, }); - }, [roomId]); -}; diff --git a/src/entities/activities/lib/use-activities.ts b/src/entities/activities/lib/use-activities.ts new file mode 100644 index 00000000..5eb0fe88 --- /dev/null +++ b/src/entities/activities/lib/use-activities.ts @@ -0,0 +1,20 @@ +import { useMemo } from 'react'; + +import { ActivitiesModel, activititesModel } from '../models'; + +export interface UseActivitiesParams { + readonly roomId: number; + + /** + * @default 50 + */ + readonly count?: number; +} + +export const useActivities = (params: UseActivitiesParams): ActivitiesModel => { + const { roomId, count, } = params; + + return useMemo(() => { + return activititesModel.create(params); + }, [roomId, count]); +}; diff --git a/src/entities/activities/models/activities/model.spec.ts b/src/entities/activities/models/activities/model.spec.ts index 30bfdf8e..98d05c15 100644 --- a/src/entities/activities/models/activities/model.spec.ts +++ b/src/entities/activities/models/activities/model.spec.ts @@ -20,7 +20,7 @@ describe('src/entities/activitites/models/activities/model', () => { let model: ActivitiesModel; const createModel = (roomId = defaultRoomId) => { - model = create({ roomId, }); + model = create({ roomId, name: 'test', }); }; beforeEach(() => { @@ -37,7 +37,7 @@ describe('src/entities/activitites/models/activities/model', () => { test('should create signleton model for the same room', () => { createModel(); - const anotherModel = create({ roomId: defaultRoomId, }); + const anotherModel = create({ roomId: defaultRoomId, name: 'test', }); expect(model).toBe(anotherModel); }); @@ -45,7 +45,7 @@ describe('src/entities/activitites/models/activities/model', () => { test('should create different models for different rooms', () => { createModel(); - const anotherModel = create({ roomId: rooms[1].id, }); + const anotherModel = create({ roomId: rooms[1].id, name: 'test', }); expect(model).not.toBe(anotherModel); }); @@ -57,7 +57,7 @@ describe('src/entities/activitites/models/activities/model', () => { track.unsubscribe(); - expect(model).not.toBe(create({ roomId: defaultRoomId, })); + expect(model).not.toBe(create({ roomId: defaultRoomId, name: 'test', })); }); test('should load all activitites', async () => { diff --git a/src/entities/activities/models/activities/model.ts b/src/entities/activities/models/activities/model.ts index 5ae7da8e..a921cd78 100644 --- a/src/entities/activities/models/activities/model.ts +++ b/src/entities/activities/models/activities/model.ts @@ -6,7 +6,8 @@ import { withRetry, reatomAsync, onConnect, - withAbort + withAbort, + withErrorAtom } from '@reatom/framework'; import { activitiesApi } from '@/shared/api'; @@ -25,46 +26,53 @@ import { FetchActivititesParams } from './types'; -const modelName = constructName('activitites', 'in-room'); +const modelName = 'list'; export const create = createSingletonFactory( (params: CreateActivitiesModelParams): ActivitiesModel => { - const { roomId, } = params; + const { name, roomId, count = 50, } = params; - const fetch = reatomAsync(async (ctx, params?: FetchActivititesParams) => { - return ctx.schedule(() => - activitiesApi.getAll({ ...params, roomId, }, ctx.controller.signal) - ); - }, constructName(modelName, 'fetch')).pipe( + const fetch = reatomAsync( + async (ctx, params?: FetchActivititesParams) => { + return ctx.schedule(() => + activitiesApi.getAll( + { ...params, roomId, count, }, + ctx.controller.signal + ) + ); + }, + constructName(name, modelName, 'fetch') + ).pipe( withDataAtom( { items: [], totalCount: 0, limit: 50, } as PaginationResponse, mapStandardResponse ), withCache(), withRetry(), - withAbort() + withAbort(), + withErrorAtom(undefined, { initState: null, }) ); const pendingAtom = atom( (ctx) => !!ctx.spy(fetch.pendingAtom), - constructName(modelName, 'pendingAtom') + constructName(name, modelName, 'pendingAtom') ); - const activititesAtom = atom( (ctx) => ctx.spy(fetch.dataAtom).items, - constructName(modelName, 'activititesAtom') + constructName(name, modelName, 'activititesAtom') ); - const hasItemsAtom = atom( (ctx) => !!ctx.spy(fetch.dataAtom).totalCount, - constructName(modelName, 'hasItemsAtom') + constructName(name, modelName, 'hasItemsAtom') ); + const pagesCountAtom = atom( + (ctx) => { + const { limit, totalCount, } = ctx.spy(fetch.dataAtom); - const pagesCountAtom = atom((ctx) => { - const { limit, totalCount, } = ctx.spy(fetch.dataAtom); - - return Math.ceil(totalCount / limit); - }, constructName(modelName, 'pagesCountAtom')); + return Math.ceil(totalCount / limit); + }, + constructName(name, modelName, 'pagesCountAtom') + ); onConnect(fetch.dataAtom, (ctx) => { fetch(ctx); @@ -84,6 +92,7 @@ export const create = createSingletonFactory( pagesCountAtom, hasItemsAtom, pendingAtom, + errorAtom: fetch.errorAtom, }; }, { diff --git a/src/entities/activities/models/activities/types.ts b/src/entities/activities/models/activities/types.ts index 62b5fd83..bd8b5b94 100644 --- a/src/entities/activities/models/activities/types.ts +++ b/src/entities/activities/models/activities/types.ts @@ -11,8 +11,6 @@ import { import { ActivityActionId, activityActionRT } from '../actions'; import { ActivitySphereId, activitySphereRT } from '../spheres'; - - export const activityRT = Record({ id: Number, roomId: Number, @@ -29,7 +27,6 @@ export type Activities = Activity[]; export interface FetchActivititesParams { readonly page?: number; - readonly count?: number; readonly by?: string | null; readonly type?: SortDirection | null; readonly before?: string | null; @@ -43,7 +40,13 @@ export interface FetchActivititesParams { } export interface CreateActivitiesModelParams { + readonly name: string; readonly roomId: number; + + /** + * @default 50 + */ + readonly count?: number; } export interface ActivitiesModel { @@ -52,6 +55,7 @@ export interface ActivitiesModel { StandardResponse> >; readonly activititesAtom: Atom; + readonly errorAtom: Atom; readonly pagesCountAtom: Atom; readonly hasItemsAtom: Atom; readonly pendingAtom: Atom; diff --git a/src/entities/activities/ui/activities-actions-picker/activities-actions-picker.tsx b/src/entities/activities/ui/activities-actions-picker/activities-actions-picker.tsx index fdf6b57d..6e96d846 100644 --- a/src/entities/activities/ui/activities-actions-picker/activities-actions-picker.tsx +++ b/src/entities/activities/ui/activities-actions-picker/activities-actions-picker.tsx @@ -13,8 +13,8 @@ import { CommonProps, PickerProps } from '@/shared/types'; import { Field, FieldProps } from '@/shared/ui'; import { useActivityActions } from '../../lib'; -import { ActivityAction } from '../../model'; -import { ActivityActionPicture } from '../activity-action-picture'; +import { ActivityAction } from '../../models'; +import { ActivityActionIcon } from '../activity-action-icon'; export type ActivitiesActionsPickerProps = CommonProps & PickerProps & @@ -55,7 +55,7 @@ export const ActivitiesActionsPicker: React.FC = return ( - + {activity} diff --git a/src/entities/activities/ui/activity-action-icon/__snapshots__/activity-action-icon.spec.tsx.snap b/src/entities/activities/ui/activity-action-icon/__snapshots__/activity-action-icon.spec.tsx.snap index 2bb92e0a..22acfd65 100644 --- a/src/entities/activities/ui/activity-action-icon/__snapshots__/activity-action-icon.spec.tsx.snap +++ b/src/entities/activities/ui/activity-action-icon/__snapshots__/activity-action-icon.spec.tsx.snap @@ -1,62 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`src/entities/activities/ui/activity-action-icon > should render icon for \`create\` action > create 1`] = ` -
    - -
    -`; - -exports[`src/entities/activities/ui/activity-action-icon > should render icon for \`remove\` action > remove 1`] = ` -
    - -
    -`; - -exports[`src/entities/activities/ui/activity-action-icon > should render icon for \`update\` action > update 1`] = ` -
    - -
    -`; - exports[`src/entities/activities/ui/activity-action-icon/activity-action-icon > should render icon for \`create\` action > create 1`] = `
    = (props) => { - const { className, } = props; - const { t, } = useTranslation('room-activities'); - - const [open, { toggleOff, toggleOn, }] = useToggle(); - const [reset, submit] = useUnit([form.resetValues, form.submit]); - - const onSubmit = usePreventDefault(() => { - submit(); - toggleOff(); - }); - - const onReset = () => { - reset(); - toggleOff(); - }; - - const titleT = t('actions.filter_activities.title'); - const submitT = t('actions.filter_activities.actions.submit'); - const resetT = t('actions.filter_activities.actions.reset'); - - const buttons = ( - <> - - - - ); - - return ( - } - slots={{ actions: buttons, }}> - {({ isPopup, }) => ( -
    - - - - - - {buttons} - - )} -
    - ); -}; - -const Action: React.FC = () => { - const { t, } = useTranslation('room-activities'); - - const label = t('actions.filter_activities.fields.action'); - - const actionIds = useUnit(form.fields.actionIds); - - return ( - - ); -}; - -const Spheres: React.FC = () => { - const { t, } = useTranslation('room-activities'); - - const label = t('actions.filter_activities.fields.spheres'); - const sphereIds = useUnit(form.fields.sphereIds); - - return ( - - ); -}; - -const Users: React.FC = () => { - const { t, } = useTranslation('room-activities'); - - const label = t('actions.filter_activities.fields.users'); - const activistIds = useUnit(form.fields.activistIds); - - return ( - - ); -}; - -const After: React.FC = () => { - const { t, } = useTranslation('common'); - - const label = t('fields.create_after'); - const after = useUnit(form.fields.after); - - return ( - - ); -}; - -const Before: React.FC = () => { - const { t, } = useTranslation('common'); - - const label = t('fields.create_before'); - const before = useUnit(form.fields.before); - - return ( - - ); -}; diff --git a/src/features/activities/activities-filters/index.ts b/src/features/activities/activities-filters/index.ts index 31b4a7ba..1d7048e0 100644 --- a/src/features/activities/activities-filters/index.ts +++ b/src/features/activities/activities-filters/index.ts @@ -1,2 +1,2 @@ -export * as activitiesFiltersModel from './model'; -export { ActivitiesFilters, type ActivitiesFiltersProps } from './filters'; +export * from './model'; +export { ActivitiesFilters, type ActivitiesFiltersProps } from './ui'; diff --git a/src/features/activities/activities-filters/lib/index.ts b/src/features/activities/activities-filters/lib/index.ts new file mode 100644 index 00000000..7812d6a4 --- /dev/null +++ b/src/features/activities/activities-filters/lib/index.ts @@ -0,0 +1 @@ +export * from './use-activity-filters'; diff --git a/src/features/activities/activities-filters/lib/use-activity-filters.ts b/src/features/activities/activities-filters/lib/use-activity-filters.ts new file mode 100644 index 00000000..cc282b83 --- /dev/null +++ b/src/features/activities/activities-filters/lib/use-activity-filters.ts @@ -0,0 +1,22 @@ +import { useMemo } from 'react'; + +import { + ActivitiesFiltersModel, + OnFiltersChanged, + activitiesFiltersModel +} from '../model'; + +export interface UseActivityFiltersParams { + readonly name: string; + readonly onFiltersChanged: OnFiltersChanged; +} + +export const useActivityFilters = ( + params: UseActivityFiltersParams +): ActivitiesFiltersModel => { + const { name, onFiltersChanged, } = params; + + return useMemo(() => { + return activitiesFiltersModel.create({ name, onFiltersChanged, }); + }, [name, onFiltersChanged]); +}; diff --git a/src/features/activities/activities-filters/model.ts b/src/features/activities/activities-filters/model.ts deleted file mode 100644 index e3d05c34..00000000 --- a/src/features/activities/activities-filters/model.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createDomain } from 'effector'; -import { createForm } from 'effector-forms'; - -import { GetActivitiesInRoomParams } from '@/shared/api'; - -const activitiesFiltersDomain = createDomain(); - -export interface ActivitiesFiltersForm - extends Required< - Omit - > {} - -export const form = createForm({ - fields: { - activistIds: { - init: [], - }, - after: { - init: null, - }, - before: { - init: null, - }, - sphereIds: { init: [], }, - actionIds: { init: [], }, - }, - domain: activitiesFiltersDomain, -}); diff --git a/src/features/activities/activities-filters/model/index.ts b/src/features/activities/activities-filters/model/index.ts new file mode 100644 index 00000000..d561d47e --- /dev/null +++ b/src/features/activities/activities-filters/model/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * as activitiesFiltersModel from './model'; diff --git a/src/features/activities/activities-filters/model/model.ts b/src/features/activities/activities-filters/model/model.ts new file mode 100644 index 00000000..cc534628 --- /dev/null +++ b/src/features/activities/activities-filters/model/model.ts @@ -0,0 +1,70 @@ +// import qs from 'qs'; +import { reatomForm } from '@reatom/form'; +// import { withSearchParamsPersist } from '@reatom/url'; + +import { constructName } from '@/shared/lib'; + +import { + ActivitiesFiltersModel, + ActivitiesFitlers, + CreateActivitiesFiltersModelParams +} from './types'; + +export const create = ( + params: CreateActivitiesFiltersModelParams +): ActivitiesFiltersModel => { + const { name, onFiltersChanged, } = params; + + const form = reatomForm( + { + actionIds: { + initState: [] satisfies ActivitiesFitlers['actionIds'], + }, + activistIds: { + initState: [] satisfies ActivitiesFitlers['activistIds'], + }, + after: null as ActivitiesFitlers['after'], + before: null as ActivitiesFitlers['before'], + sphereIds: { + initState: [] satisfies ActivitiesFitlers['sphereIds'], + }, + }, + { + onSubmit: (_ctx, state) => { + onFiltersChanged(state); + }, + resetOnSubmit: false, + name: constructName(name, 'form'), + } + ); + + form.reset.onCall((ctx) => { + onFiltersChanged(ctx.get(form.fieldsState)); + }); + + /** + * @todo It does not work + */ + // form.fieldsState.pipe( + // withSearchParamsPersist('filters', { + // replace: true, + // parse: (v = '') => + // qs.parse(v, { parseArrays: true }) as any as ActivitiesFitlers, + // serialize: (v: ActivitiesFitlers) => + // qs.stringify(v, { arrayFormat: 'brackets' }), + // }) + // ); + + const { submit, reset, } = form; + const { actionIds, activistIds, after, before, sphereIds, } = form.fields; + + return { + submit, + reset, + actionIds, + activistIds, + after, + before, + sphereIds, + }; +}; diff --git a/src/features/activities/activities-filters/model/types.ts b/src/features/activities/activities-filters/model/types.ts new file mode 100644 index 00000000..1e17026f --- /dev/null +++ b/src/features/activities/activities-filters/model/types.ts @@ -0,0 +1,29 @@ +import { FieldAtom } from '@reatom/form'; +import { Action } from '@reatom/framework'; + +import { Fn } from '@/shared/types'; + +export interface ActivitiesFitlers { + readonly actionIds: number[]; + readonly activistIds: number[]; + readonly after: string | null; + readonly before: string | null; + readonly sphereIds: number[]; +} + +export type OnFiltersChanged = Fn<[filters: ActivitiesFitlers], void>; + +export interface CreateActivitiesFiltersModelParams { + readonly name: string; + readonly onFiltersChanged: OnFiltersChanged; +} + +export interface ActivitiesFiltersModel { + readonly submit: Action; + readonly reset: Action; + readonly actionIds: FieldAtom; + readonly activistIds: FieldAtom; + readonly after: FieldAtom; + readonly before: FieldAtom; + readonly sphereIds: FieldAtom; +} diff --git a/src/features/activities/activities-filters/__snapshots__/filters.spec.tsx.snap b/src/features/activities/activities-filters/ui/__snapshots__/filters.spec.tsx.snap similarity index 86% rename from src/features/activities/activities-filters/__snapshots__/filters.spec.tsx.snap rename to src/features/activities/activities-filters/ui/__snapshots__/filters.spec.tsx.snap index 96eb9068..65352ad9 100644 --- a/src/features/activities/activities-filters/__snapshots__/filters.spec.tsx.snap +++ b/src/features/activities/activities-filters/ui/__snapshots__/filters.spec.tsx.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`features/activities/activities-filters/filters > should render buttons to open filters form > closed filters 1`] = ` +exports[`features/activities/activities-filters/ui/filters.tsx > should render buttons to open filters form > closed filters 1`] = `
    + + + ); + + return ( + } + slots={{ actions: buttons, }}> + {({ isPopup, }) => ( + + + + + + + {buttons} + + )} + + ); +}; + +interface FieldProps { + readonly field: FieldAtom; +} + +const Action: FC = (props) => { + const { field, } = props; + + const [value] = useAtom(field.value); + const [error] = useAtom((ctx) => ctx.spy(field.validation).error, [field]); + const change = useAction(field.change); + const focus = useAction(field.focus.in); + const blur = useAction(field.focus.out); + + const { t, } = useTranslation('room-activities', { + keyPrefix: 'actions.filter_activities.fields', + }); + const labelT = t('action'); + + const isError = !error; + + return ( + + ); +}; + +const Spheres: FC = (props) => { + const { field, } = props; + + const [value] = useAtom(field.value); + const [error] = useAtom((ctx) => ctx.spy(field.validation).error, [field]); + const change = useAction(field.change); + const focus = useAction(field.focus.in); + const blur = useAction(field.focus.out); + + const { t, } = useTranslation('room-activities', { + keyPrefix: 'actions.filter_activities.fields', + }); + + const labelT = t('spheres'); + + const isError = !error; + + return ( + + ); +}; + +const Users: FC = (props) => { + const { field, roomId, } = props; + + const [value] = useAtom(field.value); + const [error] = useAtom((ctx) => ctx.spy(field.validation).error, [field]); + const change = useAction(field.change); + const focus = useAction(field.focus.in); + const blur = useAction(field.focus.out); + + const { t, } = useTranslation('room-activities', { + keyPrefix: 'actions.filter_activities.fields', + }); + + const labelT = t('users'); + + const isError = !error; + + return ( + + ); +}; + +const After: FC = (props) => { + const { field, } = props; + + const [value] = useAtom(field.value); + const [error] = useAtom((ctx) => ctx.spy(field.validation).error, [field]); + const change = useAction(field.change); + const focus = useAction(field.focus.in); + const blur = useAction(field.focus.out); + + const { t, } = useTranslation('common'); + + const labelT = t('fields.create_after'); + + const isError = !error; + + return ( + + ); +}; + +const Before: FC = (props) => { + const { field, } = props; + + const [value] = useAtom(field.value); + const [error] = useAtom((ctx) => ctx.spy(field.validation).error, [field]); + const change = useAction(field.change); + const focus = useAction(field.focus.in); + const blur = useAction(field.focus.out); + + const { t, } = useTranslation('common'); + + const labelT = t('fields.create_before'); + + const isError = !error; + + return ( + + ); +}; diff --git a/src/features/activities/activities-filters/ui/index.ts b/src/features/activities/activities-filters/ui/index.ts new file mode 100644 index 00000000..31b4a7ba --- /dev/null +++ b/src/features/activities/activities-filters/ui/index.ts @@ -0,0 +1,2 @@ +export * as activitiesFiltersModel from './model'; +export { ActivitiesFilters, type ActivitiesFiltersProps } from './filters'; From 09d1e3ef2b17fadedaf0e54af1f453c3ea5f9a9e Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 16:46:44 +0400 Subject: [PATCH 19/71] refactor(shared): rewrite routes configuration with reatom --- .eslintrc.json | 63 ++-- package-lock.json | 681 +++++++++++++++++------------------ package.json | 7 +- src/shared/configs/routes.ts | 115 ++---- vite.config.ts | 8 - vitest.config.ts | 11 +- 6 files changed, 410 insertions(+), 475 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 0f7e6044..549f42b8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,9 +7,6 @@ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:sonarjs/recommended", - "plugin:effector/recommended", - "plugin:effector/react", - "plugin:effector/scope", "plugin:import/recommended", "plugin:import/warnings", "plugin:import/typescript", @@ -26,7 +23,27 @@ "ecmaVersion": "latest", "sourceType": "module" }, - "plugins": ["react", "@typescript-eslint", "effector", "@reatom"], + "plugins": ["react", "@typescript-eslint", "@reatom", "boundaries"], + "settings": { + "import/resolver": "typescript", + "boundaries/elements": [ + { "type": "app", "pattern": "@/app/*" }, + { "type": "pages", "pattern": "@/pages/*" }, + { "type": "widgets", "pattern": "@/widgets/*" }, + { "type": "features", "pattern": "@/features/*" }, + { + "type": "@x", + "pattern": "@/entities/*/@x/*.ts", + "mode": "file" + }, + { + "type": "entities", + "pattern": "@/entities/*" + }, + { "type": "shared", "pattern": "@/shared/*" } + ], + "boundaries/ignore": ["**/*.spec.*"] + }, "rules": { /* STANDARD */ "no-use-before-define": "off", @@ -86,7 +103,11 @@ }, { "message": "Private imports are prohibited, use public imports instead", - "group": ["@/entities/*/**"] + "group": [ + "@/entities/*/**", + "!@/entities/*/@x", + "!@/entities/*/@x/*" + ] }, { "message": "Private imports are prohibited, use public imports instead", @@ -150,6 +171,11 @@ "position": "after", "pattern": "@/features/**" }, + { + "group": "internal", + "position": "after", + "pattern": "@/entities/*/@x/*.ts" + }, { "group": "internal", "position": "after", @@ -186,36 +212,29 @@ /* BOUNDARIES */ "boundaries/element-types": [ - "warn", + "error", { "default": "disallow", "rules": [ { - "from": "@/app", - "allow": [ - "@/pages", - "@/widgets", - "@/features", - "@/entities", - "@/shared" - ] + "from": "app", + "allow": ["pages", "widgets", "features", "entities", "shared"] }, { - "from": "@/pages", - "allow": ["@/widgets", "@/features", "@/entities", "@/shared"] + "from": "pages", + "allow": ["widgets", "features", "entities", "shared"] }, { - "from": "@/widgets", - "allow": ["@/features", "@/entities", "@/shared"] + "from": "widgets", + "allow": ["features", "entities", "shared"] }, - { "from": "@/features", "allow": ["@/entities", "@/shared"] }, - { "from": "@/entities", "allow": ["@/shared"] }, - { "from": "@/shared", "allow": ["@/shared"] } + { "from": "features", "allow": ["entities", "shared"] }, + { "from": "entities", "allow": ["@x", "shared"] }, + { "from": "shared", "allow": ["shared"] } ] } ], - /* TS */ "@typescript-eslint/no-unused-vars": ["error"], "@typescript-eslint/no-empty-interface": ["off"], "@typescript-eslint/no-explicit-any": "warn" diff --git a/package-lock.json b/package-lock.json index 34ead715..6c4cd7d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "i18next-http-backend": "^2.2.2", "joi": "^17.11.0", "ky": "^1.1.0", - "patronum": "^1.20.0", + "path-to-regexp": "^8.2.0", "qs": "^6.14.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -47,14 +47,11 @@ "zod": "^3.24.1" }, "devDependencies": { - "@babel/cli": "^7.23.0", - "@babel/core": "^7.23.2", "@faker-js/faker": "^8.4.1", "@playwright/test": "^1.40.1", "@reatom/devtools": "^0.7.2", "@reatom/eslint-plugin": "^3.4.3", "@reatom/testing": "^3.4.7", - "@rollup/plugin-babel": "^6.0.4", "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", @@ -71,8 +68,8 @@ "eslint": "^8.51.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-typescript": "^3.7.0", "eslint-plugin-boundaries": "^3.4.0", - "eslint-plugin-effector": "^0.11.0", "eslint-plugin-import": "^2.28.1", "eslint-plugin-sonarjs": "^0.21.0", "husky": "^8.0.3", @@ -125,41 +122,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/cli": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.23.0.tgz", - "integrity": "sha512-17E1oSkGk2IwNILM4jtfAvgjt+ohmpfBky8aLerUfYZhiPNg7ca+CRCxZn8QDxwNhV/upsc2VHBCqGFIR+iBfA==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "commander": "^4.0.1", - "convert-source-map": "^2.0.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.2.0", - "make-dir": "^2.1.0", - "slash": "^2.0.0" - }, - "bin": { - "babel": "bin/babel.js", - "babel-external-helpers": "bin/babel-external-helpers.js" - }, - "engines": { - "node": ">=6.9.0" - }, - "optionalDependencies": { - "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", - "chokidar": "^3.4.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/cli/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -2571,19 +2533,21 @@ } }, "node_modules/@farfetched/core": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@farfetched/core/-/core-0.10.4.tgz", - "integrity": "sha512-sNhsuTL5/DPrpgp7JLN3rKJRxX6PHHG0h2Xlq4JjS2BzOGoJC/SB5iEr3kJyZLN4tjdXa2C0G/L7awf6sPh/4g==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@farfetched/core/-/core-0.10.6.tgz", + "integrity": "sha512-WSnQjftZ6ZfCFq+SOLvJpc52+xN4ND5azPPcuwOcygSZhKWs+P5wNH9WNpJ0KdP9ExOuol3JITze5LK2hLh+3g==", + "license": "MIT", "peerDependencies": { "effector": "^22.5.0" } }, "node_modules/@farfetched/runtypes": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@farfetched/runtypes/-/runtypes-0.10.4.tgz", - "integrity": "sha512-TRT6AfgFnwBQG3pjjMRC6UBlJ4DbNSvKILajINIJVhgt/9mmxlc8Mt+PObhpm9r0hhscPuLvsmBDKNIUk6UhnA==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@farfetched/runtypes/-/runtypes-0.10.6.tgz", + "integrity": "sha512-c7+WfJXi1eKy98B2ysaEgaDQMKIQB7dPoU21vB6fwDWD8hxU45z8/eUpRMA7DzRXZZWIytF8Rt0tMvMshL476g==", + "license": "MIT", "peerDependencies": { - "@farfetched/core": "0.10.4", + "@farfetched/core": "0.10.6", "runtypes": "^6.6.0" } }, @@ -2629,12 +2593,14 @@ "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" }, "node_modules/@hapi/topo": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" } @@ -3417,13 +3383,6 @@ "node": ">=6" } }, - "node_modules/@nicolo-ribaudo/chokidar-2": { - "version": "2.1.8-no-fsevents.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", - "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", - "dev": true, - "optional": true - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3459,6 +3418,16 @@ "node": ">= 8" } }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", @@ -3660,54 +3629,6 @@ "resolved": "https://registry.npmjs.org/@reatom/utils/-/utils-3.11.0.tgz", "integrity": "sha512-e59Gd7WC6jp5yo3LGUnveZkntsfNUJlXVkPGl24I2dFomoOH1XEPGJtDCzQQu5Y8S0mVuAUj5VwbqA4f+x4Pew==" }, - "node_modules/@rollup/plugin-babel": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz", - "integrity": "sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@rollup/pluginutils": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "@types/babel__core": { - "optional": true - }, - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", @@ -3917,9 +3838,10 @@ ] }, "node_modules/@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" } @@ -3927,12 +3849,14 @@ "node_modules/@sideway/formula": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" }, "node_modules/@sinclair/typebox": { "version": "0.27.8", @@ -4913,11 +4837,12 @@ } }, "node_modules/@withease/web-api": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@withease/web-api/-/web-api-1.0.1.tgz", - "integrity": "sha512-KC+6ueVd1rEpLvMN7uXHa7E5TKdv7MxIhEAAs8tjAgmKxkUgranpsrjU2ZlwHGimuc98Z428/n+zgldZE3nIMg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@withease/web-api/-/web-api-1.3.0.tgz", + "integrity": "sha512-ULQu1yOoKDom7kGNg2KHeb3OJm09CbNRBZfJ/H9Mpuwia7Cydw8o0vHQEtDFyPmwYm7OIhCNEw/ILZUHzNJCQQ==", + "license": "MIT", "peerDependencies": { - "effector": "^22.5.0" + "effector": "^22.5.0 || ^23.0.0" } }, "node_modules/acorn": { @@ -5233,6 +5158,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/atomic-router/-/atomic-router-0.8.0.tgz", "integrity": "sha512-jUNmxqs4zKiTWnvcgNU7u51k+wS8XHYgy7YKTTwiuNB+PPRj+sRAjE+50nyExDygRS5A7yELhT14ziIICjQoqQ==", + "license": "MIT", "dependencies": { "path-to-regexp": "^6.2.0" }, @@ -5248,6 +5174,7 @@ "version": "0.8.5", "resolved": "https://registry.npmjs.org/atomic-router-react/-/atomic-router-react-0.8.5.tgz", "integrity": "sha512-XI+L5Kt+NSbVyZ8rc2M+9i4VslETv+ASNuR3wUm537p83mkXWVqZ37taNCsVDnX2D4vLGkfj8Fl6b/hw/tBqeQ==", + "license": "MIT", "dependencies": { "clsx": "^1.1.1" }, @@ -5264,7 +5191,8 @@ "node_modules/atomic-router/node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT" }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -5819,15 +5747,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -6248,9 +6167,9 @@ "dev": true }, "node_modules/effector": { - "version": "22.8.7", - "resolved": "https://registry.npmjs.org/effector/-/effector-22.8.7.tgz", - "integrity": "sha512-vCevjxmFwnZJuRGtqjwfi+sHtu+7gK3qdbv4fxi/AsMdDtoMeoz9C3/28pjNABviZeEARj6ZfAHbNAq5i9d7RQ==", + "version": "22.8.8", + "resolved": "https://registry.npmjs.org/effector/-/effector-22.8.8.tgz", + "integrity": "sha512-uqYEPt/jIZ3Y1WhpyED1XuXAJNsVM5TcWZvcyQhl6nYKRynklGiu+Fy/BnLQftmMcxZS478laAuExUPfuOGiOg==", "funding": [ { "type": "patreon", @@ -6261,6 +6180,7 @@ "url": "https://opencollective.com/effector" } ], + "license": "MIT", "engines": { "node": ">=11.0.0" } @@ -6269,6 +6189,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/effector-forms/-/effector-forms-1.3.4.tgz", "integrity": "sha512-m/Swvhf6eDyEgYINULCXLZf9uhMMicYQs4Te3bg5Hx2WdUeq5Cts18QwHU+YxoTTJh06rptWbHk0QHHmI4uPuA==", + "license": "ISC", "peerDependencies": { "effector": ">=22.0.0 <23.0.0", "effector-react": ">=22.2.0 <23.0.0" @@ -6278,6 +6199,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/effector-localstorage/-/effector-localstorage-1.0.0.tgz", "integrity": "sha512-SyJiKCHv05GPcWAC/lupQJWvp2EwY2QRfMuiy/JmuXxw++WUi5ozvxKrPT9ntRndn3AgD3S+LOPPwl7GWi/rzw==", + "license": "MIT", "peerDependencies": { "effector": ">=22.0.0" } @@ -6286,6 +6208,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/effector-mui-snacks/-/effector-mui-snacks-1.0.0.tgz", "integrity": "sha512-AY1ZlkV5/3yVoErF/8QDX95KjotygFWHh7GgxM5p5/cD/LsV/p7HYGy2rvTxqLrlKVxNtUGEvnzux3jwHYaNzg==", + "license": "ISC", "dependencies": { "classnames": "^2.3.2", "patronum": "^1.18.0" @@ -6312,6 +6235,7 @@ "url": "https://opencollective.com/effector" } ], + "license": "MIT", "dependencies": { "use-sync-external-store": "^1.0.0" }, @@ -7047,6 +6971,91 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", + "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3", + "stable-hash": "^0.0.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/enhanced-resolve": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-import-resolver-typescript/node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eslint-module-utils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", @@ -7162,38 +7171,6 @@ "node": ">=8" } }, - "node_modules/eslint-plugin-effector": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-effector/-/eslint-plugin-effector-0.11.0.tgz", - "integrity": "sha512-iujBwqh8z09lxrnTF3jAj2WHwxdKYWzf2DXn6ambdBjx3wgHgLt2vjpzctXxWR1+PPyoj3GyQYY7tsnc/RTNTw==", - "dev": true, - "dependencies": { - "prettier": "^2.3.2" - }, - "engines": { - "node": "^14 || ^15 || ^16 || ^17 || ^18 || ^19 || ^20" - }, - "peerDependencies": { - "effector": "*", - "eslint": "7 || 8" - } - }, - "node_modules/eslint-plugin-effector/node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/eslint-plugin-import": { "version": "2.28.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", @@ -7534,12 +7511,6 @@ "node": ">=4.0" } }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -7783,12 +7754,6 @@ "node": ">=14.14" } }, - "node_modules/fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", - "dev": true - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7943,6 +7908,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -8206,6 +8184,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.7.6" } @@ -8522,6 +8501,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -9038,13 +9040,14 @@ } }, "node_modules/joi": { - "version": "17.11.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", - "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } @@ -9556,28 +9559,6 @@ "source-map-js": "^1.2.0" } }, - "node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10296,6 +10277,15 @@ "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -10323,6 +10313,7 @@ "version": "1.20.0", "resolved": "https://registry.npmjs.org/patronum/-/patronum-1.20.0.tgz", "integrity": "sha512-UsUR8nUbSPJ0kEQPNorZ/0+Rwlk+c45O4BfqlLHAdRQvx1C7ZSfC9txfPwJo+fajRtzVLebZm1m+67+Ke9SgdA==", + "license": "MIT", "peerDependencies": { "effector": "^22.1.2" } @@ -10357,15 +10348,6 @@ "node": ">=0.10" } }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/pkg-types": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", @@ -10994,6 +10976,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -11119,7 +11111,8 @@ "node_modules/runtypes": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/runtypes/-/runtypes-6.7.0.tgz", - "integrity": "sha512-3TLdfFX8YHNFOhwHrSJza6uxVBmBrEjnNQlNXvXCdItS0Pdskfg5vVXUTWIN+Y23QR09jWpSl99UHkA83m4uWA==" + "integrity": "sha512-3TLdfFX8YHNFOhwHrSJza6uxVBmBrEjnNQlNXvXCdItS0Pdskfg5vVXUTWIN+Y23QR09jWpSl99UHkA83m4uWA==", + "license": "MIT" }, "node_modules/safe-array-concat": { "version": "1.0.1", @@ -11343,15 +11336,6 @@ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "dev": true }, - "node_modules/slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", @@ -11423,6 +11407,13 @@ "deprecated": "Please use @jridgewell/sourcemap-codec instead", "dev": true }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true, + "license": "MIT" + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -14213,31 +14204,6 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, - "@babel/cli": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.23.0.tgz", - "integrity": "sha512-17E1oSkGk2IwNILM4jtfAvgjt+ohmpfBky8aLerUfYZhiPNg7ca+CRCxZn8QDxwNhV/upsc2VHBCqGFIR+iBfA==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.17", - "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", - "chokidar": "^3.4.0", - "commander": "^4.0.1", - "convert-source-map": "^2.0.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.2.0", - "make-dir": "^2.1.0", - "slash": "^2.0.0" - }, - "dependencies": { - "convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - } - } - }, "@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -15801,15 +15767,15 @@ "dev": true }, "@farfetched/core": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@farfetched/core/-/core-0.10.4.tgz", - "integrity": "sha512-sNhsuTL5/DPrpgp7JLN3rKJRxX6PHHG0h2Xlq4JjS2BzOGoJC/SB5iEr3kJyZLN4tjdXa2C0G/L7awf6sPh/4g==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@farfetched/core/-/core-0.10.6.tgz", + "integrity": "sha512-WSnQjftZ6ZfCFq+SOLvJpc52+xN4ND5azPPcuwOcygSZhKWs+P5wNH9WNpJ0KdP9ExOuol3JITze5LK2hLh+3g==", "requires": {} }, "@farfetched/runtypes": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@farfetched/runtypes/-/runtypes-0.10.4.tgz", - "integrity": "sha512-TRT6AfgFnwBQG3pjjMRC6UBlJ4DbNSvKILajINIJVhgt/9mmxlc8Mt+PObhpm9r0hhscPuLvsmBDKNIUk6UhnA==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@farfetched/runtypes/-/runtypes-0.10.6.tgz", + "integrity": "sha512-c7+WfJXi1eKy98B2ysaEgaDQMKIQB7dPoU21vB6fwDWD8hxU45z8/eUpRMA7DzRXZZWIytF8Rt0tMvMshL476g==", "requires": {} }, "@floating-ui/core": { @@ -16294,13 +16260,6 @@ } } }, - "@nicolo-ribaudo/chokidar-2": { - "version": "2.1.8-no-fsevents.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", - "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", - "dev": true, - "optional": true - }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -16327,6 +16286,12 @@ "fastq": "^1.6.0" } }, + "@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true + }, "@open-draft/deferred-promise": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", @@ -16512,27 +16477,6 @@ "resolved": "https://registry.npmjs.org/@reatom/utils/-/utils-3.11.0.tgz", "integrity": "sha512-e59Gd7WC6jp5yo3LGUnveZkntsfNUJlXVkPGl24I2dFomoOH1XEPGJtDCzQQu5Y8S0mVuAUj5VwbqA4f+x4Pew==" }, - "@rollup/plugin-babel": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz", - "integrity": "sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.18.6", - "@rollup/pluginutils": "^5.0.1" - } - }, - "@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", - "dev": true, - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - } - }, "@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", @@ -16646,9 +16590,9 @@ "optional": true }, "@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", "requires": { "@hapi/hoek": "^9.0.0" } @@ -17391,9 +17335,9 @@ } }, "@withease/web-api": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@withease/web-api/-/web-api-1.0.1.tgz", - "integrity": "sha512-KC+6ueVd1rEpLvMN7uXHa7E5TKdv7MxIhEAAs8tjAgmKxkUgranpsrjU2ZlwHGimuc98Z428/n+zgldZE3nIMg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@withease/web-api/-/web-api-1.3.0.tgz", + "integrity": "sha512-ULQu1yOoKDom7kGNg2KHeb3OJm09CbNRBZfJ/H9Mpuwia7Cydw8o0vHQEtDFyPmwYm7OIhCNEw/ILZUHzNJCQQ==", "requires": {} }, "acorn": { @@ -18032,12 +17976,6 @@ "delayed-stream": "~1.0.0" } }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true - }, "common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -18366,9 +18304,9 @@ "dev": true }, "effector": { - "version": "22.8.7", - "resolved": "https://registry.npmjs.org/effector/-/effector-22.8.7.tgz", - "integrity": "sha512-vCevjxmFwnZJuRGtqjwfi+sHtu+7gK3qdbv4fxi/AsMdDtoMeoz9C3/28pjNABviZeEARj6ZfAHbNAq5i9d7RQ==" + "version": "22.8.8", + "resolved": "https://registry.npmjs.org/effector/-/effector-22.8.8.tgz", + "integrity": "sha512-uqYEPt/jIZ3Y1WhpyED1XuXAJNsVM5TcWZvcyQhl6nYKRynklGiu+Fy/BnLQftmMcxZS478laAuExUPfuOGiOg==" }, "effector-forms": { "version": "1.3.4", @@ -18912,6 +18850,55 @@ } } }, + "eslint-import-resolver-typescript": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", + "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", + "dev": true, + "requires": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3", + "stable-hash": "^0.0.4" + }, + "dependencies": { + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "enhanced-resolve": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + } + } + }, "eslint-module-utils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", @@ -18996,23 +18983,6 @@ } } }, - "eslint-plugin-effector": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-effector/-/eslint-plugin-effector-0.11.0.tgz", - "integrity": "sha512-iujBwqh8z09lxrnTF3jAj2WHwxdKYWzf2DXn6ambdBjx3wgHgLt2vjpzctXxWR1+PPyoj3GyQYY7tsnc/RTNTw==", - "dev": true, - "requires": { - "prettier": "^2.3.2" - }, - "dependencies": { - "prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true - } - } - }, "eslint-plugin-import": { "version": "2.28.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", @@ -19193,12 +19163,6 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -19399,12 +19363,6 @@ "universalify": "^2.0.0" } }, - "fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", - "dev": true - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -19507,6 +19465,15 @@ "get-intrinsic": "^1.1.1" } }, + "get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "requires": { + "resolve-pkg-maps": "^1.0.0" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -19929,6 +19896,23 @@ "has-tostringtag": "^1.0.0" } }, + "is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "dev": true, + "requires": { + "semver": "^7.6.3" + }, + "dependencies": { + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + } + } + }, "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -20287,13 +20271,13 @@ } }, "joi": { - "version": "17.11.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", - "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "requires": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } @@ -20690,24 +20674,6 @@ "source-map-js": "^1.2.0" } }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -21202,6 +21168,11 @@ } } }, + "path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==" + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -21243,12 +21214,6 @@ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, "pkg-types": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", @@ -21686,6 +21651,12 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, + "resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true + }, "restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -21924,12 +21895,6 @@ } } }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, "slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", @@ -21983,6 +21948,12 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "dev": true }, + "stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true + }, "stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", diff --git a/package.json b/package.json index 1ab7af4b..e4aa2799 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "i18next-http-backend": "^2.2.2", "joi": "^17.11.0", "ky": "^1.1.0", - "patronum": "^1.20.0", + "path-to-regexp": "^8.2.0", "qs": "^6.14.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -57,14 +57,11 @@ "zod": "^3.24.1" }, "devDependencies": { - "@babel/cli": "^7.23.0", - "@babel/core": "^7.23.2", "@faker-js/faker": "^8.4.1", "@playwright/test": "^1.40.1", "@reatom/devtools": "^0.7.2", "@reatom/eslint-plugin": "^3.4.3", "@reatom/testing": "^3.4.7", - "@rollup/plugin-babel": "^6.0.4", "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", @@ -81,8 +78,8 @@ "eslint": "^8.51.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-typescript": "^3.7.0", "eslint-plugin-boundaries": "^3.4.0", - "eslint-plugin-effector": "^0.11.0", "eslint-plugin-import": "^2.28.1", "eslint-plugin-sonarjs": "^0.21.0", "husky": "^8.0.3", diff --git a/src/shared/configs/routes.ts b/src/shared/configs/routes.ts index f0fac0fa..78523d0a 100644 --- a/src/shared/configs/routes.ts +++ b/src/shared/configs/routes.ts @@ -1,79 +1,44 @@ -import { - createHistoryRouter, - createRoute, - createRouterControls -} from 'atomic-router'; +import { compile } from 'path-to-regexp'; -export const routes = { - rooms: { base: createRoute(), invite: createRoute(), }, - room: { - base: createRoute<{ id: number; tab: string }>(), - tasks: createRoute<{ id: number }>(), - tags: createRoute<{ id: number }>(), - activities: createRoute<{ id: number }>(), - users: createRoute<{ id: number }>(), - }, - login: createRoute(), - registration: { - base: createRoute(), - thanks: createRoute(), - activate: createRoute(), - }, - settings: createRoute(), -}; +type AnyParams = Record; -export const controls = createRouterControls(); +interface RouteInfo { + readonly pattern: string; + readonly getPath: (params: Params) => string; +} -export const router = createHistoryRouter({ - routes: [ - { - path: '/login', - route: routes.login, - }, - { - path: '/registration', - route: routes.registration.base, - }, - { - path: '/registration/thanks', - route: routes.registration.thanks, - }, - { - path: '/registration/activate', - route: routes.registration.activate, - }, - { - path: '/rooms', - route: routes.rooms.base, - }, - { - path: '/rooms/invite', - route: routes.rooms.invite, - }, - { - path: '/rooms/:id/:tab', - route: routes.room.base, - }, - { - path: '/rooms/:id/tasks', - route: routes.room.tasks, - }, - { - path: '/rooms/:id/tags', - route: routes.room.tags, - }, - { - path: '/rooms/:id/activities', - route: routes.room.activities, - }, - { - path: '/rooms/:id/users', - route: routes.room.users, +const createRouteInfo = ( + path: string +): RouteInfo => { + const toPath = compile(path); + + return { + pattern: path, + getPath(params: Params) { + return toPath(params); }, - { - path: '/settings', - route: routes.settings, - } - ], - controls, -}); + }; +}; + +type RoutesInfo = Record>; + +export const ROUTES = { + login: createRouteInfo('/login'), + registration: { + root: createRouteInfo('/registration'), + thanks: createRouteInfo('/registration/thanks'), + activate: createRouteInfo('/registration/activate'), + }, + rooms: { + root: createRouteInfo('/rooms'), + invitation: createRouteInfo('/rooms/invite'), + }, + room: { + root: createRouteInfo<{ id: string; tab: string }>('/rooms/:id/:tab'), + tasks: createRouteInfo<{ id: string }>('/rooms/:id/tasks'), + tags: createRouteInfo<{ id: string }>('/rooms/:id/tags'), + activities: createRouteInfo<{ id: string }>('/rooms/:id/activities'), + members: createRouteInfo<{ id: string }>('/rooms/:id/users'), + }, + settings: createRouteInfo('/settings'), +} satisfies Record>; diff --git a/vite.config.ts b/vite.config.ts index 1ba23cce..3e4e7eda 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ import * as path from 'node:path'; -import { babel } from '@rollup/plugin-babel'; import react from '@vitejs/plugin-react'; import { VitePWA } from 'vite-plugin-pwa'; import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite'; @@ -15,13 +14,6 @@ export default defineConfig(({ mode }) => { const plugins = [ react(), - babel({ - babelrc: true, - configFile: true, - babelHelpers: 'bundled', - browserslistConfigFile: true, - extensions: ['.ts', '.tsx'], - }), splitVendorChunkPlugin(), viteStaticCopy({ targets: [ diff --git a/vitest.config.ts b/vitest.config.ts index 68ef197e..2afc5c96 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,5 +1,4 @@ import * as path from 'node:path'; -import { babel } from '@rollup/plugin-babel'; import { defineConfig } from 'vitest/config'; export default defineConfig({ @@ -28,13 +27,5 @@ export default defineConfig({ '~/test-utils': path.resolve(__dirname, 'test-utils'), }, }, - plugins: [ - babel({ - babelrc: true, - configFile: true, - babelHelpers: 'bundled', - browserslistConfigFile: true, - extensions: ['.ts', '.tsx'], - }), - ], + plugins: [], }); From c2c6f087c9c5d31da7930e70c62cbf6c864763a9 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 16:48:52 +0400 Subject: [PATCH 20/71] refactor(activities): rewrite conmponent to open all activities without atomic-router --- .../open-all-room-activities.spec.tsx.snap | 2 +- .../open-all-room-activities.spec.tsx | 40 ++++++------------- .../open-all-room-activities.tsx | 24 +++++------ 3 files changed, 24 insertions(+), 42 deletions(-) diff --git a/src/features/activities/open-all-room-activities/__snapshots__/open-all-room-activities.spec.tsx.snap b/src/features/activities/open-all-room-activities/__snapshots__/open-all-room-activities.spec.tsx.snap index 5670eee8..39a0ce1c 100644 --- a/src/features/activities/open-all-room-activities/__snapshots__/open-all-room-activities.spec.tsx.snap +++ b/src/features/activities/open-all-room-activities/__snapshots__/open-all-room-activities.spec.tsx.snap @@ -2,7 +2,7 @@ exports[`features/activities/open-all-room-activities/open-all-room-activities > should render link looks like button 1`] = ` diff --git a/src/features/activities/open-all-room-activities/open-all-room-activities.spec.tsx b/src/features/activities/open-all-room-activities/open-all-room-activities.spec.tsx index 84640928..5ea35757 100644 --- a/src/features/activities/open-all-room-activities/open-all-room-activities.spec.tsx +++ b/src/features/activities/open-all-room-activities/open-all-room-activities.spec.tsx @@ -1,41 +1,24 @@ -import { RenderResult, fireEvent, render } from '@testing-library/react'; -import { RouterProvider } from 'atomic-router-react'; -import { Scope, allSettled, fork } from 'effector'; -import { Provider } from 'effector-react'; -import { createMemoryHistory } from 'history'; import { beforeEach, describe, expect, test } from 'vitest'; -import { router, routes } from '@/shared/configs'; +import { RenderResult, TestCtx, createTestCtx, render } from '~/test-utils'; + +import { ROUTES } from '@/shared/configs'; import { OpenAllRoomActivities } from './open-all-room-activities'; describe('features/activities/open-all-room-activities/open-all-room-activities', () => { let wrapper: RenderResult; - let scope: Scope; + let ctx: TestCtx; const roomId = 123; const createComponent = () => { - wrapper = render( - - - - - - ); + wrapper = render(, { ctx, }); }; const findLink = () => wrapper.getByRole('link', { name: 'blocks.last_activities.actions.open', }); - beforeEach(async () => { - scope = fork(); - await allSettled(router.setHistory, { - scope, - params: createMemoryHistory(), - }); - await allSettled(routes.room.tasks.open, { - scope, - params: { id: roomId, }, - }); + beforeEach(() => { + ctx = createTestCtx(); }); test('should render link looks like button', () => { @@ -44,13 +27,14 @@ describe('features/activities/open-all-room-activities/open-all-room-activities' expect(findLink()).toMatchSnapshot(); }); - test('should navigate to activities room on click', () => { + test('should navigate to activities room on click', async () => { createComponent(); const link = findLink(); - fireEvent.click(link); - - expect(scope.getState(router.$path)).toBe(`/rooms/${roomId}/activities`); + expect(link).toHaveAttribute( + 'href', + ROUTES.room.activities.getPath({ id: roomId.toString(), }) + ); }); }); diff --git a/src/features/activities/open-all-room-activities/open-all-room-activities.tsx b/src/features/activities/open-all-room-activities/open-all-room-activities.tsx index b654678c..f499a3b8 100644 --- a/src/features/activities/open-all-room-activities/open-all-room-activities.tsx +++ b/src/features/activities/open-all-room-activities/open-all-room-activities.tsx @@ -1,31 +1,29 @@ import { Button } from '@mui/material'; -import { RouteInstance } from 'atomic-router'; -import { Link } from 'atomic-router-react'; import * as React from 'react'; +import { FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { routes } from '@/shared/configs'; -import { useParam } from '@/shared/lib'; +import { ROUTES } from '@/shared/configs'; import { CommonProps } from '@/shared/types'; +export interface OpenAllRoomActivitiesProps extends CommonProps { + readonly roomId: number; +} -export const OpenAllRoomActivities: React.FC = React.memo( +export const OpenAllRoomActivities: FC = memo( (props) => { - const { className, } = props; + const { className, roomId, } = props; const { t, } = useTranslation('room-tasks'); - const roomId = useParam(routes.room.tasks, 'id'); - - const text = t('blocks.last_activities.actions.open'); + const textT = t('blocks.last_activities.actions.open'); return ( ); } From 26e2668b3be13ac88c280f23c6840e7bdbbdbb8b Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 17:33:48 +0400 Subject: [PATCH 21/71] refactor(activities): extract activities pagination into separated feature --- .../activities/activities-pagination/index.ts | 2 + .../activities-pagination/lib/index.ts | 1 + .../lib/use-pagination.ts | 16 ++ .../activities-pagination/model/index.ts | 2 + .../activities-pagination/model/model.ts | 39 +++++ .../activities-pagination/model/types.ts | 15 ++ .../ui/__snapshots__/pagination.spec.tsx.snap | 151 ++++++++++++++++++ .../activities-pagination/ui/index.ts | 1 + .../ui/pagination.spec.tsx | 89 +++++++++++ .../activities-pagination/ui/pagination.tsx | 39 +++++ src/features/activities/index.ts | 3 +- 11 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 src/features/activities/activities-pagination/index.ts create mode 100644 src/features/activities/activities-pagination/lib/index.ts create mode 100644 src/features/activities/activities-pagination/lib/use-pagination.ts create mode 100644 src/features/activities/activities-pagination/model/index.ts create mode 100644 src/features/activities/activities-pagination/model/model.ts create mode 100644 src/features/activities/activities-pagination/model/types.ts create mode 100644 src/features/activities/activities-pagination/ui/__snapshots__/pagination.spec.tsx.snap create mode 100644 src/features/activities/activities-pagination/ui/index.ts create mode 100644 src/features/activities/activities-pagination/ui/pagination.spec.tsx create mode 100644 src/features/activities/activities-pagination/ui/pagination.tsx diff --git a/src/features/activities/activities-pagination/index.ts b/src/features/activities/activities-pagination/index.ts new file mode 100644 index 00000000..60508821 --- /dev/null +++ b/src/features/activities/activities-pagination/index.ts @@ -0,0 +1,2 @@ +export type { OnPageChanged, Page } from './model'; +export * from './ui'; diff --git a/src/features/activities/activities-pagination/lib/index.ts b/src/features/activities/activities-pagination/lib/index.ts new file mode 100644 index 00000000..a372577a --- /dev/null +++ b/src/features/activities/activities-pagination/lib/index.ts @@ -0,0 +1 @@ +export * from './use-pagination'; diff --git a/src/features/activities/activities-pagination/lib/use-pagination.ts b/src/features/activities/activities-pagination/lib/use-pagination.ts new file mode 100644 index 00000000..5bf6f0b2 --- /dev/null +++ b/src/features/activities/activities-pagination/lib/use-pagination.ts @@ -0,0 +1,16 @@ +import { useMemo } from 'react'; + +import { OnPageChanged, activitiesPaginationModel } from '../model'; + +export interface UsePaginationParams { + readonly name: string; + readonly onPageChanged: OnPageChanged; +} + +export const usePagination = (params: UsePaginationParams) => { + const { name, onPageChanged, } = params; + + return useMemo(() => { + return activitiesPaginationModel.create({ name, onPageChanged, }); + }, [name, onPageChanged]); +}; diff --git a/src/features/activities/activities-pagination/model/index.ts b/src/features/activities/activities-pagination/model/index.ts new file mode 100644 index 00000000..c52cd366 --- /dev/null +++ b/src/features/activities/activities-pagination/model/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * as activitiesPaginationModel from './model'; diff --git a/src/features/activities/activities-pagination/model/model.ts b/src/features/activities/activities-pagination/model/model.ts new file mode 100644 index 00000000..e5bd6c9a --- /dev/null +++ b/src/features/activities/activities-pagination/model/model.ts @@ -0,0 +1,39 @@ +import { atom } from '@reatom/framework'; +import { withSearchParamsPersist } from '@reatom/url'; + +import { PAGE_SEARCH_PARAM_NAME } from '@/shared/configs'; +import { constructName } from '@/shared/lib'; + +import { + ActivitiesPaginationModel, + CreateActivitiesPaginationModelParams +} from './types'; + +const modelName = 'pagination'; + +export const create = ( + params: CreateActivitiesPaginationModelParams +): ActivitiesPaginationModel => { + const { name, onPageChanged, } = params; + + const pageAtom = atom( + 1, + constructName(name, modelName, 'pageAtom') + ).pipe( + withSearchParamsPersist(PAGE_SEARCH_PARAM_NAME, (page = '1') => + Number(page) + ) + ); + + pageAtom.onChange((_ctx, page) => onPageChanged({ page, })); + pageAtom.onChange((ctx) => { + ctx.schedule(() => { + window.scrollTo({ + left: 0, + top: 0, + }); + }); + }); + + return { pageAtom, }; +}; diff --git a/src/features/activities/activities-pagination/model/types.ts b/src/features/activities/activities-pagination/model/types.ts new file mode 100644 index 00000000..d990bb04 --- /dev/null +++ b/src/features/activities/activities-pagination/model/types.ts @@ -0,0 +1,15 @@ +import { AtomMut } from '@reatom/framework'; + +import { Fn } from '@/shared/types'; + +export type Page = number; +export type OnPageChanged = Fn<[{ readonly page: Page }], void>; + +export interface CreateActivitiesPaginationModelParams { + readonly name: string; + readonly onPageChanged: OnPageChanged; +} + +export interface ActivitiesPaginationModel { + readonly pageAtom: AtomMut; +} diff --git a/src/features/activities/activities-pagination/ui/__snapshots__/pagination.spec.tsx.snap b/src/features/activities/activities-pagination/ui/__snapshots__/pagination.spec.tsx.snap new file mode 100644 index 00000000..c14daf0d --- /dev/null +++ b/src/features/activities/activities-pagination/ui/__snapshots__/pagination.spec.tsx.snap @@ -0,0 +1,151 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`features/activities/activities-pagination/ui/pagination.tsx > should render pagination 1`] = ` + +`; diff --git a/src/features/activities/activities-pagination/ui/index.ts b/src/features/activities/activities-pagination/ui/index.ts new file mode 100644 index 00000000..cb727655 --- /dev/null +++ b/src/features/activities/activities-pagination/ui/index.ts @@ -0,0 +1 @@ +export * from './pagination'; diff --git a/src/features/activities/activities-pagination/ui/pagination.spec.tsx b/src/features/activities/activities-pagination/ui/pagination.spec.tsx new file mode 100644 index 00000000..0afad4e4 --- /dev/null +++ b/src/features/activities/activities-pagination/ui/pagination.spec.tsx @@ -0,0 +1,89 @@ +import { urlAtom } from '@reatom/url'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +import { RenderResult, TestCtx, createTestCtx, render } from '~/test-utils'; + +import { PAGE_SEARCH_PARAM_NAME } from '@/shared/configs'; + +import { ActivitiesPagination } from './pagination'; + +describe('features/activities/activities-pagination/ui/pagination.tsx', () => { + let wrapper: RenderResult; + let ctx: TestCtx; + const onPageChanged = vi.fn(); + const pageCount = 10; + + const createComponent = () => { + wrapper = render( + , + { + ctx, + } + ); + }; + + const findPagination = () => wrapper.getByRole('navigation'); + const findCurrentButton = (page: number) => + wrapper.getByRole('link', { name: `page ${page}`, }); + const findAnotherButton = (page: number) => + wrapper.getByRole('link', { name: `Go to page ${page}`, }); + + beforeEach(() => { + ctx = createTestCtx(); + + urlAtom.go(ctx, '/', true); + }); + + afterEach(() => { + window.location.href = '/'; + }); + + test('should render pagination', () => { + createComponent(); + + expect(findPagination()).toMatchSnapshot(); + }); + + test('should first button to be selected by default', () => { + createComponent(); + + const button = findCurrentButton(1); + + expect(button).toHaveAttribute('aria-current', 'true'); + }); + + test('should scroll page to top on page change', async () => { + createComponent(); + + const button = findAnotherButton(2); + + await wrapper.user.click(button); + + expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, left: 0, }); + }); + + test('should change page on click on another page', async () => { + createComponent(); + + const button = findAnotherButton(2); + + await wrapper.user.click(button); + + expect(onPageChanged).toHaveBeenCalledWith({ page: 2, }); + expect(button).toHaveAttribute('aria-current', 'true'); + }); + + test('should save opened page in url', async () => { + createComponent(); + + const button = findAnotherButton(2); + + await wrapper.user.click(button); + + expect(window.location.search).toContain(`${PAGE_SEARCH_PARAM_NAME}=2`); + }); +}); diff --git a/src/features/activities/activities-pagination/ui/pagination.tsx b/src/features/activities/activities-pagination/ui/pagination.tsx new file mode 100644 index 00000000..86ff5920 --- /dev/null +++ b/src/features/activities/activities-pagination/ui/pagination.tsx @@ -0,0 +1,39 @@ +import { Pagination, PaginationItem } from '@mui/material'; +import { useAtom } from '@reatom/npm-react'; +import { ChangeEvent, FC } from 'react'; + +import { CommonProps } from '@/shared/types'; + +import { usePagination } from '../lib'; +import { OnPageChanged } from '../model'; + +export interface ActivitiesPaginationProps extends CommonProps { + readonly pageCount: number; + readonly onPageChanged: OnPageChanged; +} + +export const ActivitiesPagination: FC = (props) => { + const { className, onPageChanged, pageCount, } = props; + + const model = usePagination({ name: 'activities', onPageChanged, }); + + const [page, setPage] = useAtom(model.pageAtom); + + const setNewPage = (_: ChangeEvent, page: number) => { + setPage(page); + }; + + return ( + { + return ; + }} + /> + ); +}; diff --git a/src/features/activities/index.ts b/src/features/activities/index.ts index d0263ee5..a815eb64 100644 --- a/src/features/activities/index.ts +++ b/src/features/activities/index.ts @@ -1,2 +1,3 @@ -export * from './open-all-room-activities'; export * from './activities-filters'; +export * from './activities-pagination'; +export * from './open-all-room-activities'; From eacba8cd841b8a9c089ae8f2972d10ad41f6b2de Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 20:54:31 +0400 Subject: [PATCH 22/71] refactor(users): rename user picker to members picker. Add @x exports in users segment --- src/entities/users/@x/activities.ts | 1 + src/entities/users/index.ts | 2 +- src/entities/users/lib/useSearchedUsers.ts | 2 +- src/entities/users/models/index.ts | 1 + src/entities/users/models/members/types.ts | 2 +- src/entities/users/models/users/index.ts | 1 + src/entities/users/models/users/model.ts | 0 src/entities/users/models/users/types.ts | 10 +++ src/entities/users/ui/index.ts | 2 +- src/entities/users/ui/member-picker/index.ts | 1 + .../users/ui/member-picker/member-picker.tsx | 62 +++++++++++++++++++ .../users/ui/user-search/user-search.tsx | 12 ++-- .../users/ui/users-in-room-picker/index.ts | 4 -- .../users-in-room-picker.tsx | 51 --------------- test-utils/mock-server/handlres/members.ts | 7 ++- 15 files changed, 91 insertions(+), 67 deletions(-) create mode 100644 src/entities/users/@x/activities.ts create mode 100644 src/entities/users/models/users/index.ts create mode 100644 src/entities/users/models/users/model.ts create mode 100644 src/entities/users/models/users/types.ts create mode 100644 src/entities/users/ui/member-picker/index.ts create mode 100644 src/entities/users/ui/member-picker/member-picker.tsx delete mode 100644 src/entities/users/ui/users-in-room-picker/index.ts delete mode 100644 src/entities/users/ui/users-in-room-picker/users-in-room-picker.tsx diff --git a/src/entities/users/@x/activities.ts b/src/entities/users/@x/activities.ts new file mode 100644 index 00000000..8ede32c1 --- /dev/null +++ b/src/entities/users/@x/activities.ts @@ -0,0 +1 @@ +export { userSchema } from '../models'; diff --git a/src/entities/users/index.ts b/src/entities/users/index.ts index 76671b44..84b005ff 100644 --- a/src/entities/users/index.ts +++ b/src/entities/users/index.ts @@ -1,3 +1,3 @@ export * from './ui'; export * from './lib'; -export * from './model'; +export * from './models'; diff --git a/src/entities/users/lib/useSearchedUsers.ts b/src/entities/users/lib/useSearchedUsers.ts index c8606352..a9ec3a52 100644 --- a/src/entities/users/lib/useSearchedUsers.ts +++ b/src/entities/users/lib/useSearchedUsers.ts @@ -1,6 +1,6 @@ import { useUnit } from 'effector-react'; -import { searchUserModel } from '../model'; +import { searchUserModel } from '../models'; export const useSearchedUsers = () => { return useUnit(searchUserModel.query); diff --git a/src/entities/users/models/index.ts b/src/entities/users/models/index.ts index c801bac2..f78ef6bc 100644 --- a/src/entities/users/models/index.ts +++ b/src/entities/users/models/index.ts @@ -1,2 +1,3 @@ export * from './members'; export * as searchUserModel from './search-user'; +export * from './users'; diff --git a/src/entities/users/models/members/types.ts b/src/entities/users/models/members/types.ts index 4dce8e44..500f4465 100644 --- a/src/entities/users/models/members/types.ts +++ b/src/entities/users/models/members/types.ts @@ -23,5 +23,5 @@ export interface MembersModel { readonly membersAtom: Atom; readonly pendingAtom: Atom; readonly errorAtom: Atom; - readonly retry: Action<[], Promise>>; + readonly retry: Action<[after?: number], Promise>>; } diff --git a/src/entities/users/models/users/index.ts b/src/entities/users/models/users/index.ts new file mode 100644 index 00000000..fcb073fe --- /dev/null +++ b/src/entities/users/models/users/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/src/entities/users/models/users/model.ts b/src/entities/users/models/users/model.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/entities/users/models/users/types.ts b/src/entities/users/models/users/types.ts new file mode 100644 index 00000000..f33c4923 --- /dev/null +++ b/src/entities/users/models/users/types.ts @@ -0,0 +1,10 @@ +import zod from 'zod'; + +export const userSchema = zod + .object({ + id: zod.number(), + email: zod.string().email(), + username: zod.string(), + photo: zod.string().url().nullable(), + }) + .readonly(); diff --git a/src/entities/users/ui/index.ts b/src/entities/users/ui/index.ts index 6f07efa6..15ab9ea3 100644 --- a/src/entities/users/ui/index.ts +++ b/src/entities/users/ui/index.ts @@ -1,5 +1,5 @@ export * from './template-user-list-item'; export * from './user-avatar'; -export * from './users-in-room-picker'; +export * from './member-picker'; export * from './user-search'; export * from './skeleton-user-list-item'; diff --git a/src/entities/users/ui/member-picker/index.ts b/src/entities/users/ui/member-picker/index.ts new file mode 100644 index 00000000..5f3977bb --- /dev/null +++ b/src/entities/users/ui/member-picker/index.ts @@ -0,0 +1 @@ +export { MembersPicker, type MembersPickerProps } from './member-picker'; diff --git a/src/entities/users/ui/member-picker/member-picker.tsx b/src/entities/users/ui/member-picker/member-picker.tsx new file mode 100644 index 00000000..6c20907f --- /dev/null +++ b/src/entities/users/ui/member-picker/member-picker.tsx @@ -0,0 +1,62 @@ +import { Autocomplete } from '@mui/material'; +import { useAtom } from '@reatom/npm-react'; +import * as React from 'react'; + +import { preparePickerHandler, preparePickerSelectedValue } from '@/shared/lib'; +import { CommonProps, PickerProps } from '@/shared/types'; +import { Field, FieldProps } from '@/shared/ui'; + +import { useMembersModel } from '../../lib'; +import { User, Users } from '../../models'; +import { TemplateUserListItem } from '../template-user-list-item'; + +export type MembersPickerProps = CommonProps & + PickerProps & + Omit & { + readonly roomId: number; + }; + +export const MembersPicker: React.FC = React.memo( + (props) => { + const { onChange, value, className, multiple, limitTags, roomId, ...rest } = + props; + const membersModel = useMembersModel({ roomId, }); + const [members] = useAtom(membersModel.membersAtom); + const [pending] = useAtom(membersModel.pendingAtom); + + const changeHandler = preparePickerHandler( + { multiple, onChange, }, + 'id' + ); + + const selected = preparePickerSelectedValue( + { value, multiple, }, + members, + 'id' + ); + + return ( + void} + getOptionLabel={(member) => member.username} + loading={pending} + renderOption={(params, option) => ( + + )} + renderInput={(params) => { + return ; + }} + limitTags={limitTags} + multiple={multiple} + /> + ); + } +); diff --git a/src/entities/users/ui/user-search/user-search.tsx b/src/entities/users/ui/user-search/user-search.tsx index f069c8c8..a2f9f446 100644 --- a/src/entities/users/ui/user-search/user-search.tsx +++ b/src/entities/users/ui/user-search/user-search.tsx @@ -2,17 +2,17 @@ import { Autocomplete } from '@mui/material'; import { useUnit } from 'effector-react'; import * as React from 'react'; -import { User } from '@/shared/api'; -import { CommonProps } from '@/shared/types'; +import { UserDto } from '@/shared/api'; +import { CommonProps, Fn } from '@/shared/types'; import { Field, FieldProps } from '@/shared/ui'; import { useSearchedUsers } from '../../lib'; -import { searchUserModel } from '../../model'; +import { searchUserModel } from '../../models'; import { TemplateUserListItem } from '../template-user-list-item'; export interface UserSearchProps extends CommonProps, FieldProps { - readonly onChange?: (user: User | null) => unknown; - readonly value?: User | null; + readonly onChange?: Fn<[user: UserDto | null], unknown>; + readonly value?: UserDto | null; } export const UserSearch: React.FC = (props) => { @@ -21,7 +21,7 @@ export const UserSearch: React.FC = (props) => { const resetUsers = useUnit(searchUserModel.query.reset); const searchChanged = useUnit(searchUserModel.searchChanged); - const handleChange = (_: unknown, user: User | null) => { + const handleChange = (_: unknown, user: UserDto | null) => { onChange?.(user); }; diff --git a/src/entities/users/ui/users-in-room-picker/index.ts b/src/entities/users/ui/users-in-room-picker/index.ts deleted file mode 100644 index 2bfc70fd..00000000 --- a/src/entities/users/ui/users-in-room-picker/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - UsersInRoomPicker, - type UsersInRoomPickerProps -} from './users-in-room-picker'; diff --git a/src/entities/users/ui/users-in-room-picker/users-in-room-picker.tsx b/src/entities/users/ui/users-in-room-picker/users-in-room-picker.tsx deleted file mode 100644 index 35bc34ac..00000000 --- a/src/entities/users/ui/users-in-room-picker/users-in-room-picker.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Autocomplete } from '@mui/material'; -import * as React from 'react'; - -import { User } from '@/shared/api'; -import { preparePickerHandler, preparePickerSelectedValue } from '@/shared/lib'; -import { CommonProps, PickerProps } from '@/shared/types'; -import { Field, FieldProps } from '@/shared/ui'; - -import { useUsersInRoom } from '../../lib'; -import { TemplateUserListItem } from '../template-user-list-item'; - -export type UsersInRoomPickerProps = CommonProps & - PickerProps & - Omit; - -export const UsersInRoomPicker: React.FC = React.memo( - (props) => { - const { onChange, value, className, multiple, limitTags, ...rest } = props; - const users = useUsersInRoom(); - - const changeHandler = preparePickerHandler( - { multiple, onChange, }, - 'id' - ); - - const selected = preparePickerSelectedValue( - { value, multiple, }, - users.data, - 'id' - ); - - return ( - user.username} - loading={users.pending} - renderOption={(params, option) => ( - - )} - renderInput={(params) => { - return ; - }} - limitTags={limitTags} - multiple={multiple} - /> - ); - } -); diff --git a/test-utils/mock-server/handlres/members.ts b/test-utils/mock-server/handlres/members.ts index 70c7a7fe..8154a33b 100644 --- a/test-utils/mock-server/handlres/members.ts +++ b/test-utils/mock-server/handlres/members.ts @@ -1,7 +1,7 @@ /* eslint-disable import/no-extraneous-dependencies */ import { http } from 'msw'; -import { users } from '../../fixtures'; +import { members } from '../../fixtures'; import { BASE_URL } from '../constants'; import { createUrl, createStandardResponse, notFoundError } from '../utils'; @@ -12,7 +12,7 @@ const removeMemberUrl = createUrl(baseUrl, 'remove', ':userId'); export const success = { members: http.get(getMembersUrl, () => { - return createStandardResponse(users); + return createStandardResponse(structuredClone(members)); }), exit: http.delete(exitMembersUrl, () => { return createStandardResponse(true); @@ -23,6 +23,9 @@ export const success = { }; export const error = { + members: http.get(getMembersUrl, () => { + return notFoundError; + }), exit: http.delete(exitMembersUrl, () => { return notFoundError; }), From 916409e3084f8d29e36731e779e01aabd87ff7ef Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 21:01:34 +0400 Subject: [PATCH 23/71] refactor(activities): rewrite activities lists. Add two widgets for last activities and to filtrate activities --- src/entities/activities/lib/index.ts | 2 +- src/entities/activities/lib/use-activities.ts | 5 +- .../activities/lib/use-activity-actions.ts | 3 +- .../activities/lib/use-activity-spheres.ts | 3 +- .../activities/models/actions/types.ts | 15 +- .../activities/models/activities/model.ts | 58 +- .../activities/models/activities/types.ts | 56 +- .../activities/models/spheres/types.ts | 15 +- .../activities-actions-picker.tsx | 9 +- .../activities-spheres-picker.tsx | 9 +- .../activity-list-item/activity-list-item.tsx | 2 + .../activities/activities-filters/index.ts | 2 +- .../activities-filters/model/model.ts | 2 +- .../activities-filters/ui/filters.spec.tsx | 11 +- .../activities-filters/ui/filters.tsx | 39 +- .../activities/activities-filters/ui/index.ts | 1 - .../ui/pagination.spec.tsx | 6 +- .../activities-pagination/ui/pagination.tsx | 1 + src/pages/room-activities/model.ts | 102 +- src/pages/room-activities/page.tsx | 15 +- .../activities-pagination.tsx | 41 - .../ui/activities-pagination/index.ts | 4 - .../ui/activity-list/activity-list.module.css | 0 .../activity-list.module.css.d.ts | 1 - .../ui/activity-list/activity-list.tsx | 68 - .../room-activities/ui/activity-list/index.ts | 1 - src/pages/room-activities/ui/index.ts | 1 - src/pages/room-tasks/model.ts | 84 +- src/pages/room-tasks/ui/aside/aside.tsx | 5 +- .../room-tasks/ui/last-activities/index.ts | 1 - src/shared/configs/const/routes.ts | 8 + src/shared/configs/i18n/index.ts | 6 +- src/shared/ui/date-picker/date-picker.tsx | 30 +- .../ui/section-header/section-header.spec.tsx | 6 +- .../ui/section-header/section-header.tsx | 8 +- .../activities/activities-in-room/index.ts | 1 + .../filtrable-activities.spec.tsx.snap | 2571 +++++++++++++++++ .../filtrable-activities.spec.tsx | 209 ++ .../filtrable-activities.tsx | 101 + .../ui/filtrable-activities/index.ts | 1 + .../filtrable-activities/styles.module.css} | 1 - .../styles.module.css.d.ts} | 0 .../activities/activities-in-room/ui/index.ts | 2 + .../last-room-activities.spec.tsx.snap | 424 +++ .../ui/last-room-activities/index.ts | 1 + .../last-room-activities.spec.tsx | 92 + .../last-room-activities.tsx} | 65 +- src/widgets/activities/index.ts | 1 + test-utils/fixtures/activities.ts | 55 +- test-utils/fixtures/rooms.ts | 6 +- test-utils/fixtures/tags.ts | 6 +- test-utils/fixtures/users.ts | 4 +- test-utils/mock-server/handlres/activities.ts | 49 +- test-utils/utils/render.tsx | 47 +- test-utils/utils/routing.ts | 21 +- test-utils/utils/state-manager.ts | 1 - 56 files changed, 3763 insertions(+), 515 deletions(-) delete mode 100644 src/pages/room-activities/ui/activities-pagination/activities-pagination.tsx delete mode 100644 src/pages/room-activities/ui/activities-pagination/index.ts delete mode 100644 src/pages/room-activities/ui/activity-list/activity-list.module.css delete mode 100644 src/pages/room-activities/ui/activity-list/activity-list.module.css.d.ts delete mode 100644 src/pages/room-activities/ui/activity-list/activity-list.tsx delete mode 100644 src/pages/room-activities/ui/activity-list/index.ts delete mode 100644 src/pages/room-activities/ui/index.ts delete mode 100644 src/pages/room-tasks/ui/last-activities/index.ts create mode 100644 src/widgets/activities/activities-in-room/index.ts create mode 100644 src/widgets/activities/activities-in-room/ui/filtrable-activities/__snapshots__/filtrable-activities.spec.tsx.snap create mode 100644 src/widgets/activities/activities-in-room/ui/filtrable-activities/filtrable-activities.spec.tsx create mode 100644 src/widgets/activities/activities-in-room/ui/filtrable-activities/filtrable-activities.tsx create mode 100644 src/widgets/activities/activities-in-room/ui/filtrable-activities/index.ts rename src/{pages/room-activities/page.module.css => widgets/activities/activities-in-room/ui/filtrable-activities/styles.module.css} (98%) rename src/{pages/room-activities/page.module.css.d.ts => widgets/activities/activities-in-room/ui/filtrable-activities/styles.module.css.d.ts} (100%) create mode 100644 src/widgets/activities/activities-in-room/ui/index.ts create mode 100644 src/widgets/activities/activities-in-room/ui/last-room-activities/__snapshots__/last-room-activities.spec.tsx.snap create mode 100644 src/widgets/activities/activities-in-room/ui/last-room-activities/index.ts create mode 100644 src/widgets/activities/activities-in-room/ui/last-room-activities/last-room-activities.spec.tsx rename src/{pages/room-tasks/ui/last-activities/last-activities.tsx => widgets/activities/activities-in-room/ui/last-room-activities/last-room-activities.tsx} (54%) create mode 100644 src/widgets/activities/index.ts diff --git a/src/entities/activities/lib/index.ts b/src/entities/activities/lib/index.ts index f652cd5a..75af9b34 100644 --- a/src/entities/activities/lib/index.ts +++ b/src/entities/activities/lib/index.ts @@ -1,3 +1,3 @@ export * from './use-activity-actions'; export * from './use-activity-spheres'; -export * from './use-activities-model'; +export * from './use-activities'; diff --git a/src/entities/activities/lib/use-activities.ts b/src/entities/activities/lib/use-activities.ts index 5eb0fe88..7e9c34f3 100644 --- a/src/entities/activities/lib/use-activities.ts +++ b/src/entities/activities/lib/use-activities.ts @@ -3,6 +3,7 @@ import { useMemo } from 'react'; import { ActivitiesModel, activititesModel } from '../models'; export interface UseActivitiesParams { + readonly name: string; readonly roomId: number; /** @@ -12,9 +13,9 @@ export interface UseActivitiesParams { } export const useActivities = (params: UseActivitiesParams): ActivitiesModel => { - const { roomId, count, } = params; + const { roomId, count, name, } = params; return useMemo(() => { return activititesModel.create(params); - }, [roomId, count]); + }, [roomId, count, name]); }; diff --git a/src/entities/activities/lib/use-activity-actions.ts b/src/entities/activities/lib/use-activity-actions.ts index 9801c324..f21bdea2 100644 --- a/src/entities/activities/lib/use-activity-actions.ts +++ b/src/entities/activities/lib/use-activity-actions.ts @@ -1,4 +1,5 @@ import { useAtom } from '@reatom/npm-react'; +import { useMemo } from 'react'; import { ActivityActions, activityActionsModel } from '../models'; @@ -8,7 +9,7 @@ export interface UseActivityActionsResult { } export const useActivityActions = (): UseActivityActionsResult => { - const model = activityActionsModel.create(); + const model = useMemo(activityActionsModel.create, []); const [data] = useAtom(model.actionsAtom); const [pending] = useAtom(model.pendingAtom); diff --git a/src/entities/activities/lib/use-activity-spheres.ts b/src/entities/activities/lib/use-activity-spheres.ts index 2dddac10..9b1a6675 100644 --- a/src/entities/activities/lib/use-activity-spheres.ts +++ b/src/entities/activities/lib/use-activity-spheres.ts @@ -1,4 +1,5 @@ import { useAtom } from '@reatom/npm-react'; +import { useMemo } from 'react'; import { ActivitySpheres, activitySpheresModel } from '../models'; @@ -8,7 +9,7 @@ export interface UseActivitySpheresResult { } export const useActivitySpheres = (): UseActivitySpheresResult => { - const model = activitySpheresModel.create(); + const model = useMemo(activitySpheresModel.create, []); const [data] = useAtom(model.spheresAtom); const [pending] = useAtom(model.pendingAtom); diff --git a/src/entities/activities/models/actions/types.ts b/src/entities/activities/models/actions/types.ts index 0740fb01..3f23299f 100644 --- a/src/entities/activities/models/actions/types.ts +++ b/src/entities/activities/models/actions/types.ts @@ -1,12 +1,15 @@ import { Atom } from '@reatom/framework'; -import { Number, Record, Static, String } from 'runtypes'; +import zod from 'zod'; -export const activityActionRT = Record({ - id: Number, - name: String, -}).asReadonly(); +export const activityActionSchema = zod + .object({ + id: zod.number(), + name: zod.string(), + }) + .readonly(); -export interface ActivityAction extends Static {} +export interface ActivityAction + extends zod.infer {} export type ActivityActionId = ActivityAction['id']; export type ActivityActions = ActivityAction[]; diff --git a/src/entities/activities/models/activities/model.ts b/src/entities/activities/models/activities/model.ts index a921cd78..0bf87db3 100644 --- a/src/entities/activities/models/activities/model.ts +++ b/src/entities/activities/models/activities/model.ts @@ -4,10 +4,11 @@ import { atom, onDisconnect, withRetry, - reatomAsync, - onConnect, - withAbort, - withErrorAtom + withErrorAtom, + reatomRecord, + reatomResource, + action, + withStatusesAtom } from '@reatom/framework'; import { activitiesApi } from '@/shared/api'; @@ -32,8 +33,27 @@ export const create = createSingletonFactory( (params: CreateActivitiesModelParams): ActivitiesModel => { const { name, roomId, count = 50, } = params; - const fetch = reatomAsync( - async (ctx, params?: FetchActivititesParams) => { + const paramsAtom = reatomRecord( + { + page: 1, + actionIds: [], + activistIds: [], + after: null, + before: null, + by: null, + sphereIds: [], + type: null, + }, + constructName(name, modelName, 'paramsAtom') + ); + + /** + * @todo Add `zod` validation + */ + const fetch = reatomResource( + async (ctx) => { + const params = ctx.spy(paramsAtom); + return ctx.schedule(() => activitiesApi.getAll( { ...params, roomId, count, }, @@ -49,12 +69,23 @@ export const create = createSingletonFactory( ), withCache(), withRetry(), - withAbort(), + withStatusesAtom(), withErrorAtom(undefined, { initState: null, }) ); + const changeFetchActivitiesParams = action( + (ctx, params: FetchActivititesParams) => { + if ('page' in params) { + return paramsAtom.merge(ctx, params); + } + + return paramsAtom.merge(ctx, { ...params, page: 1, }); + }, + constructName(name, modelName, 'changeFetchActivitiesParams') + ); + const pendingAtom = atom( - (ctx) => !!ctx.spy(fetch.pendingAtom), + (ctx) => ctx.spy(fetch.statusesAtom).isFirstPending, constructName(name, modelName, 'pendingAtom') ); const activititesAtom = atom( @@ -74,24 +105,21 @@ export const create = createSingletonFactory( constructName(name, modelName, 'pagesCountAtom') ); - onConnect(fetch.dataAtom, (ctx) => { - fetch(ctx); - - return () => fetch.abort(ctx); - }); - retryQuery({ query: fetch, store: activititesAtom, timeout: 5000, }); + const { retry: refetch, } = fetch; + return { - fetch, + changeFetchActivitiesParams, activititesAtom, pagesCountAtom, hasItemsAtom, pendingAtom, + refetch, errorAtom: fetch.errorAtom, }; }, diff --git a/src/entities/activities/models/activities/types.ts b/src/entities/activities/models/activities/types.ts index bd8b5b94..3f5b70af 100644 --- a/src/entities/activities/models/activities/types.ts +++ b/src/entities/activities/models/activities/types.ts @@ -1,26 +1,25 @@ -import { AsyncAction, Atom } from '@reatom/framework'; -import { Number, Record, Static, String } from 'runtypes'; - -import { user } from '@/shared/api'; -import { - PaginationResponse, - SortDirection, - StandardResponse -} from '@/shared/types'; - -import { ActivityActionId, activityActionRT } from '../actions'; -import { ActivitySphereId, activitySphereRT } from '../spheres'; - -export const activityRT = Record({ - id: Number, - roomId: Number, - activist: user, - action: activityActionRT, - sphere: activitySphereRT, - createdAt: String, -}).asReadonly(); - -export interface Activity extends Static {} +import { Action, Atom } from '@reatom/framework'; +import zod from 'zod'; + +import { userSchema } from '@/entities/users/@x/activities'; + +import { SortDirection } from '@/shared/types'; + +import { ActivityActionId, activityActionSchema } from '../actions'; +import { ActivitySphereId, activitySphereSchema } from '../spheres'; + +export const activitySchema = zod + .object({ + id: zod.number(), + roomId: zod.number(), + activist: userSchema, + action: activityActionSchema, + sphere: activitySphereSchema, + createdAt: zod.string(), + }) + .readonly(); + +export interface Activity extends zod.infer {} export type ActivityId = Activity['id']; export type Activities = Activity[]; @@ -39,6 +38,11 @@ export interface FetchActivititesParams { readonly actionIds?: ActivityActionId[]; } +type ChangeFetchActivitiesParams = Action< + [params: FetchActivititesParams], + FetchActivititesParams +>; + export interface CreateActivitiesModelParams { readonly name: string; readonly roomId: number; @@ -50,13 +54,11 @@ export interface CreateActivitiesModelParams { } export interface ActivitiesModel { - readonly fetch: AsyncAction< - [params?: FetchActivititesParams], - StandardResponse> - >; readonly activititesAtom: Atom; readonly errorAtom: Atom; readonly pagesCountAtom: Atom; readonly hasItemsAtom: Atom; readonly pendingAtom: Atom; + readonly refetch: Action; + readonly changeFetchActivitiesParams: ChangeFetchActivitiesParams; } diff --git a/src/entities/activities/models/spheres/types.ts b/src/entities/activities/models/spheres/types.ts index d4199870..f3c096c7 100644 --- a/src/entities/activities/models/spheres/types.ts +++ b/src/entities/activities/models/spheres/types.ts @@ -1,12 +1,15 @@ import { Atom } from '@reatom/framework'; -import { Number, Record, Static, String } from 'runtypes'; +import zod from 'zod'; -export const activitySphereRT = Record({ - id: Number, - name: String, -}).asReadonly(); +export const activitySphereSchema = zod + .object({ + id: zod.number(), + name: zod.string(), + }) + .readonly(); -export interface ActivitySphere extends Static {} +export interface ActivitySphere + extends zod.infer {} export type ActivitySphereId = ActivitySphere['id']; export type ActivitySpheres = ActivitySphere[]; diff --git a/src/entities/activities/ui/activities-actions-picker/activities-actions-picker.tsx b/src/entities/activities/ui/activities-actions-picker/activities-actions-picker.tsx index 6e96d846..20b372ba 100644 --- a/src/entities/activities/ui/activities-actions-picker/activities-actions-picker.tsx +++ b/src/entities/activities/ui/activities-actions-picker/activities-actions-picker.tsx @@ -5,7 +5,7 @@ import { ListItemAvatar, ListItemText } from '@mui/material'; -import * as React from 'react'; +import { FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { preparePickerHandler, preparePickerSelectedValue } from '@/shared/lib'; @@ -20,8 +20,8 @@ export type ActivitiesActionsPickerProps = CommonProps & PickerProps & Omit; -export const ActivitiesActionsPicker: React.FC = - React.memo((props) => { +export const ActivitiesActionsPicker: FC = memo( + (props) => { const { value, onChange, className, multiple, limitTags, ...rest } = props; const actions = useActivityActions(); const { t, } = useTranslation('activities'); @@ -77,4 +77,5 @@ export const ActivitiesActionsPicker: React.FC = multiple={multiple} /> ); - }); + } +); diff --git a/src/entities/activities/ui/activities-spheres-picker/activities-spheres-picker.tsx b/src/entities/activities/ui/activities-spheres-picker/activities-spheres-picker.tsx index c279232b..5589d902 100644 --- a/src/entities/activities/ui/activities-spheres-picker/activities-spheres-picker.tsx +++ b/src/entities/activities/ui/activities-spheres-picker/activities-spheres-picker.tsx @@ -1,5 +1,5 @@ import { Autocomplete, Chip, ListItem, ListItemText } from '@mui/material'; -import * as React from 'react'; +import { memo, FC } from 'react'; import { useTranslation } from 'react-i18next'; import { preparePickerHandler, preparePickerSelectedValue } from '@/shared/lib'; @@ -13,8 +13,8 @@ export type ActivitiesSpheresPickerProps = CommonProps & PickerProps & Omit; -export const ActivitiesSpheresPicker: React.FC = - React.memo((props) => { +export const ActivitiesSpheresPicker: FC = memo( + (props) => { const { value, onChange, multiple, limitTags, className, ...rest } = props; const spheres = useActivitySpheres(); const { t, } = useTranslation('activities'); @@ -67,4 +67,5 @@ export const ActivitiesSpheresPicker: React.FC = multiple={multiple} /> ); - }); + } +); diff --git a/src/entities/activities/ui/activity-list-item/activity-list-item.tsx b/src/entities/activities/ui/activity-list-item/activity-list-item.tsx index ebcb95a3..4fb6c37c 100644 --- a/src/entities/activities/ui/activity-list-item/activity-list-item.tsx +++ b/src/entities/activities/ui/activity-list-item/activity-list-item.tsx @@ -28,6 +28,8 @@ export const ActivityListItem: React.FC = (props) => { const { // eslint-disable-next-line @typescript-eslint/no-unused-vars id: _, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + roomId: _roomId, action, sphere, className, diff --git a/src/features/activities/activities-filters/index.ts b/src/features/activities/activities-filters/index.ts index 1d7048e0..da8a887a 100644 --- a/src/features/activities/activities-filters/index.ts +++ b/src/features/activities/activities-filters/index.ts @@ -1,2 +1,2 @@ -export * from './model'; +export type { OnFiltersChanged, ActivitiesFitlers } from './model'; export { ActivitiesFilters, type ActivitiesFiltersProps } from './ui'; diff --git a/src/features/activities/activities-filters/model/model.ts b/src/features/activities/activities-filters/model/model.ts index cc534628..24766a5c 100644 --- a/src/features/activities/activities-filters/model/model.ts +++ b/src/features/activities/activities-filters/model/model.ts @@ -51,7 +51,7 @@ export const create = ( // parse: (v = '') => // qs.parse(v, { parseArrays: true }) as any as ActivitiesFitlers, // serialize: (v: ActivitiesFitlers) => - // qs.stringify(v, { arrayFormat: 'brackets' }), + // queryString.stringify(v, { arrayFormat: 'brackets' }), // }) // ); diff --git a/src/features/activities/activities-filters/ui/filters.spec.tsx b/src/features/activities/activities-filters/ui/filters.spec.tsx index b8909e01..029444aa 100644 --- a/src/features/activities/activities-filters/ui/filters.spec.tsx +++ b/src/features/activities/activities-filters/ui/filters.spec.tsx @@ -1,9 +1,5 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { deviceInfoModel } from '@/shared/models'; - -import { ActivitiesFilters } from './filters'; - import { RenderResult, TestCtx, @@ -15,6 +11,11 @@ import { waitFor } from '~/test-utils'; +import { deviceInfoModel } from '@/shared/models'; + +import { ActivitiesFilters } from './filters'; + + describe('features/activities/activities-filters/ui/filters.tsx', () => { const roomId = 123; const onFiltersChanged = vi.fn(); @@ -162,4 +163,6 @@ describe('features/activities/activities-filters/ui/filters.tsx', () => { }) ); }); + + test.todo('should sync fitlers with href'); }); diff --git a/src/features/activities/activities-filters/ui/filters.tsx b/src/features/activities/activities-filters/ui/filters.tsx index 57aad103..1570cce6 100644 --- a/src/features/activities/activities-filters/ui/filters.tsx +++ b/src/features/activities/activities-filters/ui/filters.tsx @@ -1,12 +1,13 @@ /* eslint-disable sonarjs/no-duplicate-string */ import TuneIcon from '@mui/icons-material/Tune'; import { Button } from '@mui/material'; -import { FieldAtom } from '@reatom/form'; import { useAction, useAtom } from '@reatom/npm-react'; import cn from 'classnames'; -import { FC } from 'react'; +import { FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { FieldAtom } from '@reatom/form'; + import { ActivitiesActionsPicker, ActivitiesSpheresPicker @@ -48,10 +49,10 @@ export const ActivitiesFilters: FC = (props) => { toggleOff(); }); - const onReset = () => { + const onReset = usePreventDefault(() => { reset(); toggleOff(); - }; + }); const titleT = t('title'); const submitT = t('actions.submit'); @@ -106,7 +107,7 @@ interface FieldProps { readonly field: FieldAtom; } -const Action: FC = (props) => { +const Action: FC = memo((props) => { const { field, } = props; const [value] = useAtom(field.value); @@ -120,7 +121,7 @@ const Action: FC = (props) => { }); const labelT = t('action'); - const isError = !error; + const isError = !!error; return ( = (props) => { fullWidth /> ); -}; +}); -const Spheres: FC = (props) => { +const Spheres: FC = memo((props) => { const { field, } = props; const [value] = useAtom(field.value); @@ -154,7 +155,7 @@ const Spheres: FC = (props) => { const labelT = t('spheres'); - const isError = !error; + const isError = !!error; return ( = (props) => { fullWidth /> ); -}; +}); -const Users: FC = (props) => { +const Users: FC = memo((props) => { const { field, roomId, } = props; const [value] = useAtom(field.value); @@ -188,7 +189,7 @@ const Users: FC = (props) => { const labelT = t('users'); - const isError = !error; + const isError = !!error; return ( = (props) => { multiple /> ); -}; +}); -const After: FC = (props) => { +const After: FC = memo((props) => { const { field, } = props; const [value] = useAtom(field.value); @@ -220,7 +221,7 @@ const After: FC = (props) => { const labelT = t('fields.create_after'); - const isError = !error; + const isError = !!error; return ( = (props) => { name='after' /> ); -}; +}); -const Before: FC = (props) => { +const Before: FC = memo((props) => { const { field, } = props; const [value] = useAtom(field.value); @@ -249,7 +250,7 @@ const Before: FC = (props) => { const labelT = t('fields.create_before'); - const isError = !error; + const isError = !!error; return ( = (props) => { name='before' /> ); -}; +}); diff --git a/src/features/activities/activities-filters/ui/index.ts b/src/features/activities/activities-filters/ui/index.ts index 31b4a7ba..c1d88e04 100644 --- a/src/features/activities/activities-filters/ui/index.ts +++ b/src/features/activities/activities-filters/ui/index.ts @@ -1,2 +1 @@ -export * as activitiesFiltersModel from './model'; export { ActivitiesFilters, type ActivitiesFiltersProps } from './filters'; diff --git a/src/features/activities/activities-pagination/ui/pagination.spec.tsx b/src/features/activities/activities-pagination/ui/pagination.spec.tsx index 0afad4e4..c8d6ec0c 100644 --- a/src/features/activities/activities-pagination/ui/pagination.spec.tsx +++ b/src/features/activities/activities-pagination/ui/pagination.spec.tsx @@ -1,5 +1,5 @@ import { urlAtom } from '@reatom/url'; -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { RenderResult, TestCtx, createTestCtx, render } from '~/test-utils'; @@ -38,10 +38,6 @@ describe('features/activities/activities-pagination/ui/pagination.tsx', () => { urlAtom.go(ctx, '/', true); }); - afterEach(() => { - window.location.href = '/'; - }); - test('should render pagination', () => { createComponent(); diff --git a/src/features/activities/activities-pagination/ui/pagination.tsx b/src/features/activities/activities-pagination/ui/pagination.tsx index 86ff5920..eea1d021 100644 --- a/src/features/activities/activities-pagination/ui/pagination.tsx +++ b/src/features/activities/activities-pagination/ui/pagination.tsx @@ -23,6 +23,7 @@ export const ActivitiesPagination: FC = (props) => { setPage(page); }; + // @todo Add localization for aria-labels of pagination return ( params.id); - -export const page = createQueryModel({ - name: getParams.page, - defaultValue: '1', - route: authorizedRoute, -}); - -const { fields, formValidated, reset, $values, } = activitiesFiltersModel.form; - -const formApplied = createEvent(); - -const queries = [ - activitiesInRoomModel.query, - usersInRoomModel.query, - roomsModel.query -]; +const queries = [roomsModel.query]; const sorting = { by: 'createdAt', type: 'desc', } as const; -sample({ - clock: [formValidated, reset], - target: formApplied, -}); - -sample({ - clock: formApplied, - target: page.reset, -}); - -sample({ - clock: [formApplied, page.$value], - source: { - roomId: $roomId, - page: page.$value, - values: $values, - }, - fn: ({ roomId, values, page, }): GetActivitiesInRoomParams => { - return { - roomId, - ...sorting, - ...values, - page: page ? Number(page) : 1, - }; - }, - target: activitiesInRoomModel.query.start, -}); - sample({ clock: authorizedRoute.opened, fn: ({ params, query, }): GetActivitiesInRoomParams => ({ roomId: params.id, - activistIds: query[getParams.userId], - actionIds: query[getParams.actionId], - after: query[getParams.after], - before: query[getParams.before], - sphereIds: query[getParams.sphereId], - count: query[getParams.count], - page: query[getParams.page], + activistIds: query[SEARCH_PARAMS_NAMES.userId], + actionIds: query[SEARCH_PARAMS_NAMES.actionId], + after: query[SEARCH_PARAMS_NAMES.after], + before: query[SEARCH_PARAMS_NAMES.before], + sphereIds: query[SEARCH_PARAMS_NAMES.sphereId], + count: query[SEARCH_PARAMS_NAMES.count], + page: query[SEARCH_PARAMS_NAMES.page], ...sorting, }), target: queries.map((query) => query.start).concat(roomModel.query.start), }); -sample({ - clock: authorizedRoute.closed, - target: reset, -}); - sample({ clock: authorizedRoute.closed, target: queries.map((query) => query.reset), }); -querySync({ - controls, - source: { - [getParams.userId]: fields.activistIds.$value, - [getParams.actionId]: fields.actionIds.$value, - [getParams.after]: fields.after.$value, - [getParams.before]: fields.before.$value, - [getParams.sphereId]: fields.sphereIds.$value, - }, - clock: formApplied, - route: authorizedRoute, -}); +// @todo move to filters settings +// querySync({ +// controls, +// source: { +// [SEARCH_PARAMS_NAMES.userId]: fields.activistIds.$value, +// [SEARCH_PARAMS_NAMES.actionId]: fields.actionIds.$value, +// [SEARCH_PARAMS_NAMES.after]: fields.after.$value, +// [SEARCH_PARAMS_NAMES.before]: fields.before.$value, +// [SEARCH_PARAMS_NAMES.sphereId]: fields.sphereIds.$value, +// }, +// clock: formApplied, +// route: authorizedRoute, +// }); diff --git a/src/pages/room-activities/page.tsx b/src/pages/room-activities/page.tsx index c8429c9e..15ea05ee 100644 --- a/src/pages/room-activities/page.tsx +++ b/src/pages/room-activities/page.tsx @@ -3,27 +3,22 @@ import cn from 'classnames'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { ActivitiesFilters } from '@/features/activities'; +import { FiltrableActivities } from '@/widgets/activities'; import { usePageTitle } from '@/shared/lib'; import { CommonProps } from '@/shared/types'; -import { SectionHeader } from '@/shared/ui'; - -import styles from './page.module.css'; -import { ActivityList } from './ui'; const ActivitiesPage: React.FC = React.memo((props) => { const { className, } = props; const { t, } = useTranslation('room-activities'); - const title = t('title'); + const titleT = t('title'); - usePageTitle(title); + usePageTitle(titleT); return ( - - } /> - + + ); }); diff --git a/src/pages/room-activities/ui/activities-pagination/activities-pagination.tsx b/src/pages/room-activities/ui/activities-pagination/activities-pagination.tsx deleted file mode 100644 index 4ee21752..00000000 --- a/src/pages/room-activities/ui/activities-pagination/activities-pagination.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Pagination, PaginationItem } from '@mui/material'; -import { useUnit } from 'effector-react'; -import * as React from 'react'; - -import { activitiesInRoomModel } from '@/entities/activities'; - -import { CommonProps } from '@/shared/types'; - -import { page } from '../../model'; - -export interface ActivitiesPaginationProps extends CommonProps {} - -export const ActivitiesPagination: React.FC = ( - props -) => { - const { className, } = props; - - const pageQuery = useUnit(page); - const hasItems = useUnit(activitiesInRoomModel.$hasItems); - const pageCount = useUnit(activitiesInRoomModel.$pageCount); - - return hasItems ? ( - { - window.scrollTo({ - left: 0, - top: 0, - }); - pageQuery.set(page.toString()); - }} - color='primary' - size='large' - renderItem={(item) => { - return ; - }} - /> - ) : null; -}; diff --git a/src/pages/room-activities/ui/activities-pagination/index.ts b/src/pages/room-activities/ui/activities-pagination/index.ts deleted file mode 100644 index a28a5ad4..00000000 --- a/src/pages/room-activities/ui/activities-pagination/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - ActivitiesPagination, - type ActivitiesPaginationProps -} from './activities-pagination'; diff --git a/src/pages/room-activities/ui/activity-list/activity-list.module.css b/src/pages/room-activities/ui/activity-list/activity-list.module.css deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pages/room-activities/ui/activity-list/activity-list.module.css.d.ts b/src/pages/room-activities/ui/activity-list/activity-list.module.css.d.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/src/pages/room-activities/ui/activity-list/activity-list.module.css.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/pages/room-activities/ui/activity-list/activity-list.tsx b/src/pages/room-activities/ui/activity-list/activity-list.tsx deleted file mode 100644 index 625a5e3e..00000000 --- a/src/pages/room-activities/ui/activity-list/activity-list.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import ReplayIcon from '@mui/icons-material/Replay'; -import { useUnit } from 'effector-react'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { - SkeletonActivityListItem, - ActivityListItem, - activitiesInRoomModel -} from '@/entities/activities'; - -import { routes } from '@/shared/configs'; -import { useParam } from '@/shared/lib'; -import { CommonProps } from '@/shared/types'; -import { FriendlyList, TextWithAction } from '@/shared/ui'; - -import { ActivitiesPagination } from '../activities-pagination'; - -export interface ActivityListProps extends CommonProps {} - -export const ActivityList: React.FC = (props) => { - const { className, } = props; - const { t, } = useTranslation('activities'); - - const emptyText = t('list.empty_text'); - - const hasItems = useUnit(activitiesInRoomModel.$hasItems); - - return ( - data.items} - getKey={(item) => item.id} - skeletonsCount={50} - ErrorComponent={Error} - ItemComponent={ActivityListItem} - SkeletonComponent={SkeletonActivityListItem} - emptyText={emptyText} - slots={{ - after: hasItems ? : null, - }} - /> - ); -}; - -const Error: React.FC = () => { - const { t, } = useTranslation('activities'); - - const actionText = t('actions.retry', { ns: 'common', }); - const text = t('actions.retry_actions.text'); - - const roomId = useParam(routes.room.tasks, 'id'); - const start = useUnit(activitiesInRoomModel.query.start); - - const onRetry = React.useCallback(() => { - start({ roomId, }); - }, [roomId]); - - return ( - } - /> - ); -}; diff --git a/src/pages/room-activities/ui/activity-list/index.ts b/src/pages/room-activities/ui/activity-list/index.ts deleted file mode 100644 index b68095a5..00000000 --- a/src/pages/room-activities/ui/activity-list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ActivityList, type ActivityListProps } from './activity-list'; diff --git a/src/pages/room-activities/ui/index.ts b/src/pages/room-activities/ui/index.ts deleted file mode 100644 index f30df2b1..00000000 --- a/src/pages/room-activities/ui/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './activity-list'; diff --git a/src/pages/room-tasks/model.ts b/src/pages/room-tasks/model.ts index 2666de89..f1e311e6 100644 --- a/src/pages/room-tasks/model.ts +++ b/src/pages/room-tasks/model.ts @@ -1,7 +1,6 @@ -import { cache, createQuery, update } from '@farfetched/core'; -import { runtypeContract } from '@farfetched/runtypes'; -import { RouteQuery, querySync } from 'atomic-router'; -import { createDomain, sample } from 'effector'; +import { update } from '@farfetched/core'; +import { RouteQuery } from 'atomic-router'; +import { sample } from 'effector'; import { dragTaskModel } from '@/widgets/tasks'; @@ -16,62 +15,26 @@ import { progressesModel } from '@/entities/progresses'; import { roomModel, roomsModel } from '@/entities/rooms'; import { tagsModel } from '@/entities/tags'; import { tasksInRoomModel } from '@/entities/tasks'; -import { usersInRoomModel } from '@/entities/users'; -import { - ActivityDto, - UpdateTaskParams, - activitiesApi, - activity -} from '@/shared/api'; -import { controls, SEARCH_PARAMS_NAMES, routes } from '@/shared/configs'; -import { extractData } from '@/shared/lib'; +import { UpdateTaskParams } from '@/shared/api'; +import { SEARCH_PARAMS_NAMES } from '@/shared/configs'; import { sessionModel } from '@/shared/models'; -import { - InRoomParams, - StandardResponse, - PaginationResponse, - getStandardResponse, - getPaginationResponse -} from '@/shared/types'; + +const routes = {}; export const currentRoute = routes.room.tasks; export const authorizedRoute = sessionModel.chainAuthorized(currentRoute, { otherwise: routes.login.open, }); -const { formValidated, reset, fields, } = tasksFiltersModel.form; - -const activitiesDomain = createDomain(); -const handlerFx = activitiesDomain.effect< - InRoomParams, - StandardResponse> ->(({ roomId, }) => - activitiesApi.getAll({ roomId, count: 6, by: 'createdAt', type: 'desc', }) -); -const $roomId = authorizedRoute.$params.map((params) => params.id); +const { formValidated, reset, } = tasksFiltersModel.form; -export const query = createQuery< - InRoomParams, - StandardResponse>, - Error, - StandardResponse>, - PaginationResponse ->({ - initialData: { items: [], totalCount: 0, limit: 5, }, - effect: handlerFx, - contract: runtypeContract( - getStandardResponse(getPaginationResponse(activity)) - ), - mapData: extractData, -}); +const $roomId = authorizedRoute.$params.map((params) => params.id); const queries = [ tasksInRoomModel.query, tagsModel.query, roomsModel.query, - usersInRoomModel.query, - progressesModel.query, - query + progressesModel.query ]; const mapQuery = (query: RouteQuery) => { @@ -83,8 +46,6 @@ const mapQuery = (query: RouteQuery) => { }; }; -cache(query); - sample({ clock: [$roomId, authorizedRoute.opened], source: { query: authorizedRoute.$query, roomId: $roomId, }, @@ -95,17 +56,18 @@ sample({ target: queries.map((query) => query.start).concat(roomModel.query.start), }); -querySync({ - controls, - source: { - [SEARCH_PARAMS_NAMES.userId]: fields.authorIds.$value, - [SEARCH_PARAMS_NAMES.tagId]: fields.tagIds.$value, - [SEARCH_PARAMS_NAMES.after]: fields.after.$value, - [SEARCH_PARAMS_NAMES.before]: fields.before.$value, - }, - clock: [formValidated, reset], - route: authorizedRoute, -}); +// @todo Move to tasks filters model +// querySync({ +// controls, +// source: { +// [SEARCH_PARAMS_NAMES.userId]: fields.authorIds.$value, +// [SEARCH_PARAMS_NAMES.tagId]: fields.tagIds.$value, +// [SEARCH_PARAMS_NAMES.after]: fields.after.$value, +// [SEARCH_PARAMS_NAMES.before]: fields.before.$value, +// }, +// clock: [formValidated, reset], +// route: authorizedRoute, +// }); sample({ clock: [formValidated, reset], @@ -134,7 +96,7 @@ sample({ target: updateTaskModel.mutation.start, }); -const queriesForUpdate = [progressesModel.query, query]; +const queriesForUpdate = [progressesModel.query]; [ updateTaskModel.mutation, diff --git a/src/pages/room-tasks/ui/aside/aside.tsx b/src/pages/room-tasks/ui/aside/aside.tsx index ff804bdc..86010078 100644 --- a/src/pages/room-tasks/ui/aside/aside.tsx +++ b/src/pages/room-tasks/ui/aside/aside.tsx @@ -1,9 +1,10 @@ import cn from 'classnames'; import * as React from 'react'; +import { LastRoomActivities } from '@/widgets/activities'; + import { CommonProps } from '@/shared/types'; -import { LastActivities } from '../last-activities'; import { TasksProgress } from '../tasks-progresses'; import styles from './aside.module.css'; @@ -18,7 +19,7 @@ export const Aside: React.FC = (props) => { return (
    - +
    ); }; diff --git a/src/pages/room-tasks/ui/last-activities/index.ts b/src/pages/room-tasks/ui/last-activities/index.ts deleted file mode 100644 index fc3f8406..00000000 --- a/src/pages/room-tasks/ui/last-activities/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LastActivities, type LastActivitiesProps } from './last-activities'; diff --git a/src/shared/configs/const/routes.ts b/src/shared/configs/const/routes.ts index 758f8a72..b2e5c2d8 100644 --- a/src/shared/configs/const/routes.ts +++ b/src/shared/configs/const/routes.ts @@ -1,3 +1,6 @@ +/** + * @deprecated + */ export const SEARCH_PARAMS_NAMES = { popup: 'popup', taskStatus: 'task-status', @@ -14,6 +17,11 @@ export const SEARCH_PARAMS_NAMES = { page: 'p', } as const; +export const PAGE_SEARCH_PARAM_NAME = 'p'; + +/** + * @deprecated + */ export const POPUPS_NAMES = { createTask: 'create-task', updateTask: 'update-task', diff --git a/src/shared/configs/i18n/index.ts b/src/shared/configs/i18n/index.ts index 2c48577d..21ac8dea 100644 --- a/src/shared/configs/i18n/index.ts +++ b/src/shared/configs/i18n/index.ts @@ -3,6 +3,8 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import Backend from 'i18next-http-backend'; import { initReactI18next } from 'react-i18next'; +import { __DEV__ } from '../const'; + i18n .use(Backend) .use(LanguageDetector) @@ -10,7 +12,9 @@ i18n .init({ fallbackLng: 'ru', partialBundledLanguages: true, - debug: import.meta.env.DEV, + debug: __DEV__, + appendNamespaceToCIMode: true, + appendNamespaceToMissingKey: true, interpolation: { escapeValue: false, }, diff --git a/src/shared/ui/date-picker/date-picker.tsx b/src/shared/ui/date-picker/date-picker.tsx index 02a36fef..c5c165cd 100644 --- a/src/shared/ui/date-picker/date-picker.tsx +++ b/src/shared/ui/date-picker/date-picker.tsx @@ -10,7 +10,13 @@ import { CommonProps } from '@/shared/types'; import { Field, FieldProps } from '../field'; -type FieldKeys = 'value' | 'onBlur' | 'name' | 'isValid' | 'helperText'; +type FieldKeys = + | 'value' + | 'onBlur' + | 'onFocus' + | 'name' + | 'isError' + | 'helperText'; export interface DatePickerProps extends CommonProps, @@ -21,8 +27,16 @@ export interface DatePickerProps export const DatePicker = React.memo( (props: DatePickerProps): React.ReactElement => { - const { onChange, value, isValid, name, onBlur, helperText, ...rest } = - props; + const { + onChange, + value, + isError, + name, + onBlur, + onFocus, + helperText, + ...rest + } = props; const preparedValue = dayjs(value); const handleChange: MUIDatePIckerProps['onChange'] = (date) => { let newDate: string | null; @@ -44,11 +58,19 @@ export const DatePicker = React.memo( params.onBlur?.(...args); }; + const handleFocus = (...args: any[]) => { + onFocus?.(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + params.onFocus?.(...args); + }; + return ( diff --git a/src/shared/ui/section-header/section-header.spec.tsx b/src/shared/ui/section-header/section-header.spec.tsx index 1ff7407f..400484b3 100644 --- a/src/shared/ui/section-header/section-header.spec.tsx +++ b/src/shared/ui/section-header/section-header.spec.tsx @@ -1,9 +1,9 @@ import { describe, expect, test } from 'vitest'; -import { SectionHeader, SectionHeaderProps } from './section-header'; - import { render, RenderResult } from '~/test-utils'; +import { SectionHeader, SectionHeaderProps } from './section-header'; + describe('shared/ui/section-header/section-header', () => { let wrapper: RenderResult; @@ -24,7 +24,7 @@ describe('shared/ui/section-header/section-header', () => { }); test('should render header with title and actions', () => { - createComponent({ ...defaultProps, actions:
    , }); + createComponent({ ...defaultProps, slots: { actions:
    , }, }); expect(findHeader()).toMatchSnapshot('with actions'); }); diff --git a/src/shared/ui/section-header/section-header.tsx b/src/shared/ui/section-header/section-header.tsx index 6684cbac..b6954d33 100644 --- a/src/shared/ui/section-header/section-header.tsx +++ b/src/shared/ui/section-header/section-header.tsx @@ -2,17 +2,21 @@ import { Typography } from '@mui/material'; import cn from 'classnames'; import * as React from 'react'; -import { CommonProps } from '@/shared/types'; +import { CommonProps, Slots } from '@/shared/types'; import styles from './section-header.module.css'; export interface SectionHeaderProps extends CommonProps { readonly title: string; + readonly slots?: Slots<'actions'>; readonly actions?: React.ReactElement | null; } export const SectionHeader: React.FC = (props) => { - const { title, actions, className, } = props; + const { title, className, slots = {}, } = props; + + const { actions, } = slots; + return (
    diff --git a/src/widgets/activities/activities-in-room/index.ts b/src/widgets/activities/activities-in-room/index.ts new file mode 100644 index 00000000..5ecdd1f3 --- /dev/null +++ b/src/widgets/activities/activities-in-room/index.ts @@ -0,0 +1 @@ +export * from './ui'; diff --git a/src/widgets/activities/activities-in-room/ui/filtrable-activities/__snapshots__/filtrable-activities.spec.tsx.snap b/src/widgets/activities/activities-in-room/ui/filtrable-activities/__snapshots__/filtrable-activities.spec.tsx.snap new file mode 100644 index 00000000..3d55d2f0 --- /dev/null +++ b/src/widgets/activities/activities-in-room/ui/filtrable-activities/__snapshots__/filtrable-activities.spec.tsx.snap @@ -0,0 +1,2571 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`widgets/activities/activities-in-room/ui/filtrable-activities/filtrable-activities.tsx > should render empty list with text if there is no items > empty list 1`] = ` +
    +
    +

    + title +

    +
    + +
    +
    +
    +
    +

    + list.empty_text +

    +
    +
    + +
    +
    +
    +`; + +exports[`widgets/activities/activities-in-room/ui/filtrable-activities/filtrable-activities.tsx > should render widget with header, filters, activities list and pagination > activities 1`] = ` +
    +
    +

    + title +

    +
    + +
    +
    +
    +
    +
    +
    +
      +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    +
    +
    +
    +
    + +
    +
    +
    +`; + +exports[`widgets/activities/activities-in-room/ui/filtrable-activities/filtrable-activities.tsx > should show error message if error occured during request > error 1`] = ` +
    +
    +

    + title +

    +
    + +
    +
    +
    +
    +
    +

    + actions.retry_actions.text +

    + +
    +
    +
    +
    +`; diff --git a/src/widgets/activities/activities-in-room/ui/filtrable-activities/filtrable-activities.spec.tsx b/src/widgets/activities/activities-in-room/ui/filtrable-activities/filtrable-activities.spec.tsx new file mode 100644 index 00000000..94ee9340 --- /dev/null +++ b/src/widgets/activities/activities-in-room/ui/filtrable-activities/filtrable-activities.spec.tsx @@ -0,0 +1,209 @@ +import { urlAtom } from '@reatom/url'; +import { beforeEach, describe, expect, test } from 'vitest'; + +import { + RenderResult, + TestCtx, + act, + createTestCtx, + defaultRoom, + handlers, + render, + screen, + server, + users, + waitFor +} from '~/test-utils'; + +import { FiltrableActivities } from './filtrable-activities'; + +describe('widgets/activities/activities-in-room/ui/filtrable-activities/filtrable-activities.tsx', () => { + let ctx: TestCtx; + let wrapper: RenderResult; + + const createComponent = () => { + wrapper = render( + , + { ctx, } + ); + }; + + const findRoot = () => wrapper.container.querySelector('section')!; + const findOpenButton = () => + wrapper.getByRole('button', { name: 'actions.filter_activities.title', }); + const findSubmitButton = () => + wrapper.getByRole('button', { + name: 'actions.filter_activities.actions.submit', + }); + const findActionsField = () => + wrapper.getByRole('combobox', { + name: 'actions.filter_activities.fields.action', + }); + const findUsersField = () => + wrapper.getByRole('combobox', { + name: 'actions.filter_activities.fields.users', + }); + const findActivities = () => wrapper.getAllByRole('listitem'); + const findActivityCardsByType = (type: string) => + wrapper.getAllByText(`type.${type}`); + const findPaginationButton = (page: number) => + wrapper.getByRole('link', { name: `Go to page ${page}`, }); + const findCurrentPaginationButton = (page: number) => + wrapper.getByRole('link', { name: `page ${page}`, }); + const findRetryButton = () => + wrapper.getByRole('button', { name: 'actions.retry', }); + const openFilters = async () => { + const button = findOpenButton(); + + await wrapper.user.click(button); + }; + + beforeEach(() => { + ctx = createTestCtx(); + + urlAtom.go(ctx, '/', true); + }); + + test('should render widget with header, filters, activities list and pagination', async () => { + await act(async () => createComponent()); + + expect(findRoot()).toMatchSnapshot('activities'); + }); + + test('should render empty list with text if there is no items', async () => { + await act(async () => createComponent()); + + await openFilters(); + const usersPicker = findUsersField(); + + await act(async () => { + await wrapper.user.click(usersPicker); + await wrapper.user.keyboard(users[1].username); + await waitFor(async () => { + await wrapper.user.click(screen.getByRole('option')); + }); + }); + + await wrapper.user.click(findSubmitButton()); + + await waitFor(() => { + expect(wrapper.getByText('list.empty_text')).toBeInTheDocument(); + }); + + expect(findRoot()).toMatchSnapshot('empty list'); + }); + + test('should show error message if error occured during request', async () => { + server.use(handlers.activities.error.getAll); + + await act(async () => createComponent()); + + await waitFor(() => { + expect( + wrapper.getByText('actions.retry_actions.text') + ).toBeInTheDocument(); + }); + + expect(findRoot()).toMatchSnapshot('error'); + }); + + test('should render filtred list when filters has been set', async () => { + await act(async () => createComponent()); + + await openFilters(); + const actionField = findActionsField(); + + await act(async () => { + await wrapper.user.click(actionField); + await wrapper.user.keyboard('create'); + await waitFor(async () => { + await wrapper.user.click(screen.getByRole('option')); + }); + }); + + await wrapper.user.click(findSubmitButton()); + + expect(() => findActivityCardsByType('updated')).toThrow(); + expect(() => findActivityCardsByType('removed')).toThrow(); + }); + + test('should render another page of activities', async () => { + await act(async () => createComponent()); + + const activities = findActivities(); + + await wrapper.user.click(findPaginationButton(2)); + + const anotherPageActivities = findActivities(); + + expect(activities).not.toStrictEqual(anotherPageActivities); + }); + + test('should render another page of activities with applied filters', async () => { + await act(async () => createComponent()); + + await openFilters(); + const actionField = findActionsField(); + + await act(async () => { + await wrapper.user.click(actionField); + await wrapper.user.keyboard('create'); + await waitFor(async () => { + await wrapper.user.click(screen.getByRole('option')); + }); + }); + + await wrapper.user.click(findSubmitButton()); + await wrapper.user.click(findPaginationButton(2)); + + expect(() => findActivityCardsByType('updated')).toThrow(); + expect(() => findActivityCardsByType('removed')).toThrow(); + }); + + test.skip('should reset page on filters change', async () => { + await act(async () => createComponent()); + + await wrapper.user.click(findPaginationButton(2)); + + await waitFor(() => { + expect(findCurrentPaginationButton(2)).toBeInTheDocument(); + }); + + await openFilters(); + const actionField = findActionsField(); + + await act(async () => { + await wrapper.user.click(actionField); + await wrapper.user.keyboard('create'); + await waitFor(async () => { + await wrapper.user.click(screen.getByRole('option')); + }); + }); + + await wrapper.user.click(findSubmitButton()); + + await waitFor(() => { + expect(findCurrentPaginationButton(1)).toBeInTheDocument(); + }); + }); + + test.skip('should refetch data on click refetch button', async () => { + server.use(handlers.activities.error.getAll); + + await act(async () => createComponent()); + + await waitFor(() => { + expect( + wrapper.getByText('actions.retry_actions.text') + ).toBeInTheDocument(); + }); + + server.use(handlers.activities.success.getAll); + + await wrapper.user.click(findRetryButton()); + + await waitFor(() => { + expect(findActivityCardsByType('create')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/widgets/activities/activities-in-room/ui/filtrable-activities/filtrable-activities.tsx b/src/widgets/activities/activities-in-room/ui/filtrable-activities/filtrable-activities.tsx new file mode 100644 index 00000000..1ee48371 --- /dev/null +++ b/src/widgets/activities/activities-in-room/ui/filtrable-activities/filtrable-activities.tsx @@ -0,0 +1,101 @@ +import ReplayIcon from '@mui/icons-material/Replay'; +import { useAction, useAtom } from '@reatom/npm-react'; +import cn from 'classnames'; +import { FC, memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { ActivitiesFilters, ActivitiesPagination } from '@/features/activities'; + +import { + ActivityListItem, + SkeletonActivityListItem, + useActivities +} from '@/entities/activities'; + +import { CommonProps } from '@/shared/types'; +import { FriendlyList, SectionHeader, TextWithAction } from '@/shared/ui'; + +import styles from './styles.module.css'; + +export interface FiltrableActivitiesProps extends CommonProps { + readonly roomId: number; +} + +export const FiltrableActivities: FC = memo( + (props) => { + const { className, roomId, } = props; + + const { t, } = useTranslation('activities'); + + const model = useActivities({ + name: 'acitivies-in-room', + roomId, + count: 50, + }); + const changeFetchActivitiesParams = useAction( + model.changeFetchActivitiesParams + ); + const [pageCount] = useAtom(model.pagesCountAtom); + const [hasItems] = useAtom(model.hasItemsAtom); + + const emptyT = t('list.empty_text'); + const titleT = t('title'); + + return ( +
    + + ), + }} + /> + item.id} + pendingAtom={model.pendingAtom} + skeletonsCount={50} + ErrorComponent={Error} + ItemComponent={ActivityListItem} + SkeletonComponent={SkeletonActivityListItem} + emptyText={emptyT} + slots={{ + after: hasItems ? ( + + ) : null, + }} + /> +
    + ); + } +); + +const Error: FC = () => { + const { t, } = useTranslation('activities'); + + const actionText = t('actions.retry', { ns: 'common', }); + const textT = t('actions.retry_actions.text'); + + /** + * @todo Implement refetch + */ + const refetch = useAction(() => console.log('refetch'), []); + + return ( + } + /> + ); +}; diff --git a/src/widgets/activities/activities-in-room/ui/filtrable-activities/index.ts b/src/widgets/activities/activities-in-room/ui/filtrable-activities/index.ts new file mode 100644 index 00000000..15ad7caf --- /dev/null +++ b/src/widgets/activities/activities-in-room/ui/filtrable-activities/index.ts @@ -0,0 +1 @@ +export * from './filtrable-activities'; diff --git a/src/pages/room-activities/page.module.css b/src/widgets/activities/activities-in-room/ui/filtrable-activities/styles.module.css similarity index 98% rename from src/pages/room-activities/page.module.css rename to src/widgets/activities/activities-in-room/ui/filtrable-activities/styles.module.css index 7af1457b..6752246c 100644 --- a/src/pages/room-activities/page.module.css +++ b/src/widgets/activities/activities-in-room/ui/filtrable-activities/styles.module.css @@ -1,7 +1,6 @@ .wrapper { display: grid; gap: 1.5em; - padding: 0; } diff --git a/src/pages/room-activities/page.module.css.d.ts b/src/widgets/activities/activities-in-room/ui/filtrable-activities/styles.module.css.d.ts similarity index 100% rename from src/pages/room-activities/page.module.css.d.ts rename to src/widgets/activities/activities-in-room/ui/filtrable-activities/styles.module.css.d.ts diff --git a/src/widgets/activities/activities-in-room/ui/index.ts b/src/widgets/activities/activities-in-room/ui/index.ts new file mode 100644 index 00000000..78e22ac3 --- /dev/null +++ b/src/widgets/activities/activities-in-room/ui/index.ts @@ -0,0 +1,2 @@ +export * from './filtrable-activities'; +export * from './last-room-activities'; diff --git a/src/widgets/activities/activities-in-room/ui/last-room-activities/__snapshots__/last-room-activities.spec.tsx.snap b/src/widgets/activities/activities-in-room/ui/last-room-activities/__snapshots__/last-room-activities.spec.tsx.snap new file mode 100644 index 00000000..38f34e64 --- /dev/null +++ b/src/widgets/activities/activities-in-room/ui/last-room-activities/__snapshots__/last-room-activities.spec.tsx.snap @@ -0,0 +1,424 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`widgets/activities/activities-in-room/ui/last-room-activities/last-room-activities.tsx > should render empty list with text if there is no items > empty list 1`] = ` +
    +
    +

    + blocks.last_activities.title +

    +
    +
    +

    + list.empty_text +

    +
    +
    +
    +`; + +exports[`widgets/activities/activities-in-room/ui/last-room-activities/last-room-activities.tsx > should render title, list with 6 items and button to go to all list > last activities 1`] = ` +
    +
    +

    + blocks.last_activities.title +

    +
    +
    +
    +
    +
      +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    • +
      +
      + +
      +
      +
      + + card.text + +

      + +

      +
      +
    • +
    +
    +
    +
    + +
    +`; + +exports[`widgets/activities/activities-in-room/ui/last-room-activities/last-room-activities.tsx > should show error message if error occured during request > error 1`] = ` +
    +
    +

    + blocks.last_activities.title +

    +
    +
    +
    +

    + actions.retry_actions.text +

    + +
    +
    + +
    +`; diff --git a/src/widgets/activities/activities-in-room/ui/last-room-activities/index.ts b/src/widgets/activities/activities-in-room/ui/last-room-activities/index.ts new file mode 100644 index 00000000..11b0abe0 --- /dev/null +++ b/src/widgets/activities/activities-in-room/ui/last-room-activities/index.ts @@ -0,0 +1 @@ +export * from './last-room-activities'; diff --git a/src/widgets/activities/activities-in-room/ui/last-room-activities/last-room-activities.spec.tsx b/src/widgets/activities/activities-in-room/ui/last-room-activities/last-room-activities.spec.tsx new file mode 100644 index 00000000..8a42c24b --- /dev/null +++ b/src/widgets/activities/activities-in-room/ui/last-room-activities/last-room-activities.spec.tsx @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, test } from 'vitest'; + +import { + RenderResult, + TestCtx, + act, + createTestCtx, + defaultRoom, + handlers, + render, + rooms, + server, + waitFor +} from '~/test-utils'; + +import { + LastRoomActivities, + LastRoomActivitiesProps +} from './last-room-activities'; + +describe('widgets/activities/activities-in-room/ui/last-room-activities/last-room-activities.tsx', () => { + let ctx: TestCtx; + let wrapper: RenderResult; + + const defaultProps: LastRoomActivitiesProps = { + roomId: defaultRoom.id, + className: 'classnames', + disableBorder: false, + }; + + const createComponent = (props?: Partial) => { + wrapper = render(, { + ctx, + }); + }; + + const findRoot = () => wrapper.container.querySelector('section')!; + const findActivityCardsByType = (type: string) => + wrapper.getAllByText(`type.${type}`); + const findRetryButton = () => + wrapper.getByRole('button', { name: 'actions.retry', }); + + beforeEach(() => { + ctx = createTestCtx(); + }); + + test('should render title, list with 6 items and button to go to all list', async () => { + await act(async () => createComponent()); + + expect(findRoot()).toMatchSnapshot('last activities'); + }); + + test('should render empty list with text if there is no items', async () => { + await act(async () => createComponent({ roomId: rooms[1].id, })); + + expect(findRoot()).toMatchSnapshot('empty list'); + }); + + test('should show error message if error occured during request', async () => { + server.use(handlers.activities.error.getAll); + + await act(async () => createComponent()); + + await waitFor(() => { + expect( + wrapper.getByText('actions.retry_actions.text') + ).toBeInTheDocument(); + }); + + expect(findRoot()).toMatchSnapshot('error'); + }); + + test.skip('should refetch data on click refetch button', async () => { + server.use(handlers.activities.error.getAll); + + await act(async () => createComponent()); + + await waitFor(() => { + expect( + wrapper.getByText('actions.retry_actions.text') + ).toBeInTheDocument(); + }); + + server.use(handlers.activities.success.getAll); + + await wrapper.user.click(findRetryButton()); + + await waitFor(() => { + expect(findActivityCardsByType('create')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/pages/room-tasks/ui/last-activities/last-activities.tsx b/src/widgets/activities/activities-in-room/ui/last-room-activities/last-room-activities.tsx similarity index 54% rename from src/pages/room-tasks/ui/last-activities/last-activities.tsx rename to src/widgets/activities/activities-in-room/ui/last-room-activities/last-room-activities.tsx index dfc01e76..ee2df476 100644 --- a/src/pages/room-tasks/ui/last-activities/last-activities.tsx +++ b/src/widgets/activities/activities-in-room/ui/last-room-activities/last-room-activities.tsx @@ -1,56 +1,59 @@ import ReplayIcon from '@mui/icons-material/Replay'; import { Typography } from '@mui/material'; -import { useUnit } from 'effector-react'; -import * as React from 'react'; +import { useAction } from '@reatom/npm-react'; +import { FC, useId } from 'react'; import { useTranslation } from 'react-i18next'; import { OpenAllRoomActivities } from '@/features/activities'; import { ActivityListItem, - SkeletonActivityListItem + SkeletonActivityListItem, + useActivities } from '@/entities/activities'; -import { routes } from '@/shared/configs'; -import { useParam } from '@/shared/lib'; import { CommonProps } from '@/shared/types'; import { FriendlyList, TextWithAction } from '@/shared/ui'; -import { query } from '../../model'; - -export interface LastActivitiesProps extends CommonProps { +export interface LastRoomActivitiesProps extends CommonProps { + readonly roomId: number; readonly disableBorder?: boolean; } -export const LastActivities: React.FC = (props) => { - const { className, disableBorder, } = props; +export const LastRoomActivities: FC = (props) => { + const { roomId, className, disableBorder, } = props; + + const id = useId(); + const model = useActivities({ + name: 'last-activities', + roomId, + count: 6, + }); + const { t, } = useTranslation('activities'); - const id = React.useId(); - const emptyText = t('list.empty_text'); - const title = t('blocks.last_activities.title', { ns: 'room-tasks', }); + const emptyT = t('list.empty_text'); + const titleT = t('blocks.last_activities.title', { ns: 'room-tasks', }); - /** - * @todo - */ return ( data.items} + dataAtom={model.activititesAtom} + errorAtom={model.errorAtom} getKey={(item) => item.id} + pendingAtom={model.pendingAtom} skeletonsCount={6} ErrorComponent={Error} ItemComponent={ActivityListItem} SkeletonComponent={SkeletonActivityListItem} - emptyText={emptyText} + emptyText={emptyT} slots={{ before: ( - {title} + {titleT} ), - after: , + after: , }} disableBorder={disableBorder} rootProps={{ @@ -61,24 +64,22 @@ export const LastActivities: React.FC = (props) => { ); }; -const Error: React.FC = () => { +const Error: FC = () => { const { t, } = useTranslation('activities'); - const roomId = useParam(routes.room.tasks, 'id'); - const start = useUnit(query.start); - - const onRetry = React.useCallback(() => { - start({ roomId, }); - }, [roomId]); - const actionText = t('actions.retry', { ns: 'common', }); - const text = t('actions.retry_actions.text'); + const textT = t('actions.retry_actions.text'); + + /** + * @todo Implement refetch + */ + const refetch = useAction(() => console.log('refetch'), []); return ( } /> ); diff --git a/src/widgets/activities/index.ts b/src/widgets/activities/index.ts new file mode 100644 index 00000000..24217710 --- /dev/null +++ b/src/widgets/activities/index.ts @@ -0,0 +1 @@ +export * from './activities-in-room'; diff --git a/test-utils/fixtures/activities.ts b/test-utils/fixtures/activities.ts index 7477ed57..5a7a6550 100644 --- a/test-utils/fixtures/activities.ts +++ b/test-utils/fixtures/activities.ts @@ -37,29 +37,32 @@ export const spheres: ActivitySphereDto[] = [ } ]; -export const activities: ActivityDto[] = [ - { - id: 1, - action: actions[0], - sphere: spheres[0], - roomId: defaultRoom.id, - activist: defaultUser, - createdAt: '2022-11-12T12:28:01', - }, - { - id: 2, - action: actions[1], - sphere: spheres[0], - roomId: defaultRoom.id, - activist: defaultUser, - createdAt: '2022-11-13T12:28:01', - }, - { - id: 3, - action: actions[1], - sphere: spheres[1], - roomId: defaultRoom.id, - activist: defaultUser, - createdAt: '2022-11-13T14:28:01', - } -]; +export const activities: ActivityDto[] = new Array(60) + .fill(0) + .map((_, index) => [ + { + id: index * 3 + 1, + action: actions[0], + sphere: spheres[0], + roomId: defaultRoom.id, + activist: defaultUser, + createdAt: '2022-11-12T12:28:01', + }, + { + id: index * 3 + 2, + action: actions[1], + sphere: spheres[0], + roomId: defaultRoom.id, + activist: defaultUser, + createdAt: '2022-11-13T12:28:01', + }, + { + id: index * 3 + 3, + action: actions[1], + sphere: spheres[1], + roomId: defaultRoom.id, + activist: defaultUser, + createdAt: '2022-11-13T14:28:01', + } + ]) + .flat(); diff --git a/test-utils/fixtures/rooms.ts b/test-utils/fixtures/rooms.ts index 673ea4b5..52c36a32 100644 --- a/test-utils/fixtures/rooms.ts +++ b/test-utils/fixtures/rooms.ts @@ -1,9 +1,9 @@ -import { Room } from '@/shared/api'; +import { RoomDto } from '@/shared/api'; import { generateId } from './generate-id'; import { defaultUser } from './users'; -export const rooms: Room[] = [ +export const rooms: RoomDto[] = [ { id: 1, ownerId: defaultUser.id, @@ -22,7 +22,7 @@ export const rooms: Room[] = [ export const defaultRoom = rooms[0]; -export const createRoom = (room?: Partial): Room => { +export const createRoom = (room?: Partial): RoomDto => { return { ...defaultRoom, id: generateId(), diff --git a/test-utils/fixtures/tags.ts b/test-utils/fixtures/tags.ts index de63de47..2806c0d1 100644 --- a/test-utils/fixtures/tags.ts +++ b/test-utils/fixtures/tags.ts @@ -1,8 +1,8 @@ -import { Tag } from '@/shared/api'; +import { TagDto } from '@/shared/api'; import { generateId } from './generate-id'; -export const tags: Tag[] = [ +export const tags: TagDto[] = [ { id: 1, roomId: 1, @@ -28,7 +28,7 @@ export const tags: Tag[] = [ export const defaultTag = tags[0]; -export const createTag = (tag?: Partial): Tag => { +export const createTag = (tag?: Partial): TagDto => { return { ...defaultTag, id: generateId(), diff --git a/test-utils/fixtures/users.ts b/test-utils/fixtures/users.ts index c60764d5..252613e1 100644 --- a/test-utils/fixtures/users.ts +++ b/test-utils/fixtures/users.ts @@ -1,6 +1,6 @@ -import { User } from '@/shared/api'; +import { UserDto } from '@/shared/api'; -export const users: User[] = [ +export const users: UserDto[] = [ { id: 1, email: 'email@example.org', diff --git a/test-utils/mock-server/handlres/activities.ts b/test-utils/mock-server/handlres/activities.ts index cc0dddf2..35e442a5 100644 --- a/test-utils/mock-server/handlres/activities.ts +++ b/test-utils/mock-server/handlres/activities.ts @@ -6,7 +6,8 @@ import { BASE_URL } from '../constants'; import { createPaginationResponse, createStandardResponse, - createUrl + createUrl, + internalServerError } from '../utils'; const baseUrl = createUrl(BASE_URL, 'activities'); @@ -18,22 +19,26 @@ function filterActivities( roomId: string, actionIds: string[], sphereIds: string[], - activistIds: string[] + activistIds: string[], + page: number, + count: number ) { - return activities.filter((activity) => { - return ( - activity.roomId === +roomId && - (actionIds.length - ? actionIds.includes(activity.action.id.toString()) - : true) && - (sphereIds.length - ? sphereIds.includes(activity.sphere.id.toString()) - : true) && - (activistIds.length - ? activistIds.includes(activity.activist.id.toString()) - : true) - ); - }); + return activities + .filter((activity) => { + return ( + activity.roomId === +roomId && + (actionIds.length + ? actionIds.includes(activity.action.id.toString()) + : true) && + (sphereIds.length + ? sphereIds.includes(activity.sphere.id.toString()) + : true) && + (activistIds.length + ? activistIds.includes(activity.activist.id.toString()) + : true) + ); + }) + .slice((page - 1) * count, count); } export const success = { @@ -46,6 +51,8 @@ export const success = { getAll: http.get(getAllUrl, ({ params, request, }) => { const { roomId, } = params; const url = new URL(request.url); + const count = (url.searchParams.getAll('count') ?? 50) as number; + const page = (url.searchParams.getAll('page') ?? 1) as number; const actionIds = (url.searchParams.getAll('actionIds') ?? []) as string[]; const sphereIds = (url.searchParams.getAll('sphereIds') ?? []) as string[]; const activistIds = (url.searchParams.getAll('activistIds') ?? @@ -55,11 +62,19 @@ export const success = { roomId as string, actionIds, sphereIds, - activistIds + activistIds, + +page, + +count ); return createPaginationResponse(filtered); }), }; +export const error = { + getAll: http.get(getAllUrl, () => { + return internalServerError; + }), +}; + export const standard = Object.values(success); diff --git a/test-utils/utils/render.tsx b/test-utils/utils/render.tsx index a670b275..089c0b9b 100644 --- a/test-utils/utils/render.tsx +++ b/test-utils/utils/render.tsx @@ -12,8 +12,6 @@ import { RenderHookResult as RTLRenderHookResult } from '@testing-library/react'; import userEvent, { UserEvent } from '@testing-library/user-event'; -import { RouterProvider } from 'atomic-router-react'; -import { Provider as StoreProvider } from 'effector-react'; import React, { ComponentType, Fragment, @@ -22,37 +20,28 @@ import React, { ReactNode } from 'react'; -import { router as appRouter } from '@/shared/configs'; - -import { HistoryRouter } from './routing'; -import { createTestCtx, fork, Scope, TestCtx } from './state-manager'; +import { createTestCtx, TestCtx } from './state-manager'; interface CreateAllProvidersOptions { - readonly scope: Scope; readonly ctx: TestCtx; - readonly router: HistoryRouter; readonly wrapper: JSXElementConstructor; } const createAllProviders = ( options: CreateAllProvidersOptions ): ComponentType => { - const { router, ctx, scope, wrapper: Wrapper, } = options; + const { ctx, wrapper: Wrapper, } = options; return (props) => { const { children, } = props; return ( - - - - - {children} - - - - + + + {children} + + ); }; @@ -67,15 +56,9 @@ interface RenderResult extends RTLRenderResult { } const render = (ui: ReactNode, options: RenderOptions = {}): RenderResult => { - const { - scope = fork(), - router = appRouter, - wrapper = Fragment, - ctx = createTestCtx(), - ...rest - } = options; + const { wrapper = Fragment, ctx = createTestCtx(), ...rest } = options; - const AllProviders = createAllProviders({ scope, router, wrapper, ctx, }); + const AllProviders = createAllProviders({ wrapper, ctx, }); const defaultResult = rtlRender(ui, { ...rest, wrapper: AllProviders, }); @@ -98,15 +81,9 @@ const renderHook = ( render: (initialProps: Props) => Result, options: RenderHookOptions = {} ): RenderHookResult => { - const { - scope = fork(), - router = appRouter, - wrapper = Fragment, - ctx = createTestCtx(), - ...rest - } = options; - - const AllProviders = createAllProviders({ scope, router, wrapper, ctx, }); + const { wrapper = Fragment, ctx = createTestCtx(), ...rest } = options; + + const AllProviders = createAllProviders({ wrapper, ctx, }); return rtlRenderHook(render, { ...rest, wrapper: AllProviders, }); }; diff --git a/test-utils/utils/routing.ts b/test-utils/utils/routing.ts index a4e79d05..5a980490 100644 --- a/test-utils/utils/routing.ts +++ b/test-utils/utils/routing.ts @@ -1,20 +1 @@ -import { createHistoryRouter } from 'atomic-router'; -import { MemoryHistoryOptions, createMemoryHistory } from 'history'; -import { Scope, allSettled } from './state-manager'; - -export type HistoryRouter = ReturnType; - -export interface UseTestRouterParams { - readonly router: HistoryRouter; - readonly scope: Scope; - readonly options?: MemoryHistoryOptions; -} - -export const useTestRouter = async (params: UseTestRouterParams) => { - const { scope, router, options } = params; - - await allSettled(router.setHistory, { - scope, - params: createMemoryHistory(options), - }); -}; +export const NOT_IMPLEMENTED = 'old implementation has been removed'; diff --git a/test-utils/utils/state-manager.ts b/test-utils/utils/state-manager.ts index 929873f9..44ea625b 100644 --- a/test-utils/utils/state-manager.ts +++ b/test-utils/utils/state-manager.ts @@ -7,5 +7,4 @@ const createTestCtx = (options?: CtxOptions): TestCtx => { }; export * from '@reatom/testing'; -export { Scope, allSettled, fork, scopeBind } from 'effector'; export { createTestCtx }; From 49b4242c71086d6814007916721f7c3eac7f4627 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 21:05:02 +0400 Subject: [PATCH 24/71] fix(tools): add missing dependencies --- package-lock.json | 633 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 4 +- 2 files changed, 593 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6c4cd7d3..dcdfbbfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,10 +39,11 @@ "joi": "^17.11.0", "ky": "^1.1.0", "path-to-regexp": "^8.2.0", - "qs": "^6.14.0", + "query-string": "^9.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^13.3.1", + "react-use": "^17.6.0", "runtypes": "^6.7.0", "zod": "^3.24.1" }, @@ -57,7 +58,6 @@ "@testing-library/user-event": "^14.5.2", "@types/compose-function": "^0.0.32", "@types/node": "^20.8.7", - "@types/qs": "^6.9.18", "@types/react": "^18.2.31", "@types/react-dom": "^18.2.14", "@typescript-eslint/eslint-plugin": "^6.8.0", @@ -2906,8 +2906,7 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -4215,6 +4214,12 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", @@ -4255,13 +4260,6 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, - "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/react": { "version": "18.2.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", @@ -4845,6 +4843,12 @@ "effector": "^22.5.0 || ^23.0.0" } }, + "node_modules/@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", @@ -5389,6 +5393,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5402,6 +5407,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5796,6 +5802,15 @@ "node": ">= 0.6" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/core-js-compat": { "version": "3.36.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz", @@ -5882,6 +5897,15 @@ "node": ">=12 || >=16" } }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "license": "MIT", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, "node_modules/css-mediaquery": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", @@ -6024,6 +6048,15 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, + "node_modules/decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -6150,6 +6183,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6329,6 +6363,15 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/es-abstract": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", @@ -6386,6 +6429,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6395,6 +6439,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6404,6 +6449,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7552,8 +7598,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -7583,6 +7628,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, "node_modules/fast-uri": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", @@ -7598,6 +7648,12 @@ "node": ">= 4.9.1" } }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==", + "license": "MIT" + }, "node_modules/fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -7661,6 +7717,18 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -7841,6 +7909,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -7871,6 +7940,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -8054,6 +8124,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8139,6 +8210,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8166,6 +8238,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -8290,6 +8363,12 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" + }, "node_modules/i18next": { "version": "23.6.0", "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.6.0.tgz", @@ -8428,6 +8507,15 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "node_modules/inline-style-prefixer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", + "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", + "license": "MIT", + "dependencies": { + "css-in-js-utils": "^3.1.0" + } + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -9052,6 +9140,12 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9563,6 +9657,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9891,6 +9986,60 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/nano-css": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz", + "integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==", + "license": "Unlicense", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "css-tree": "^1.1.2", + "csstype": "^3.1.2", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^7.0.1", + "rtl-css-js": "^1.16.1", + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/nano-css/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nano-css/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, + "node_modules/nano-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nano-css/node_modules/stylis": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.5.tgz", + "integrity": "sha512-K7npNOKGRYuhAFFzkzMGfxFDpN6gDwf8hcMiE+uveTVbBgm93HrNP3ZDUpKqzZ4pG7TP6fmb+EMAQPjq9FqqvA==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -9994,6 +10143,7 @@ "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10664,19 +10814,21 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", + "node_modules/query-string": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.1.tgz", + "integrity": "sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==", + "license": "MIT", "dependencies": { - "side-channel": "^1.1.0" + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" }, "engines": { - "node": ">=0.6" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/querystringify": { @@ -10787,6 +10939,41 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "peerDependencies": { + "react": "*", + "tslib": "*" + } + }, + "node_modules/react-use": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.6.0.tgz", + "integrity": "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==", + "license": "Unlicense", + "dependencies": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.6.2", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -10952,6 +11139,12 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", @@ -11085,6 +11278,15 @@ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", "dev": true }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -11192,6 +11394,18 @@ "loose-envify": "^1.1.0" } }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -11210,6 +11424,15 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", + "license": "Unlicense", + "engines": { + "node": ">=6.9" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11235,6 +11458,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -11254,6 +11478,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -11270,6 +11495,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -11288,6 +11514,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -11407,6 +11634,18 @@ "deprecated": "Please use @jridgewell/sourcemap-codec instead", "dev": true }, + "node_modules/split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stable-hash": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", @@ -11414,12 +11653,57 @@ "dev": true, "license": "MIT" }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "license": "MIT", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-gps/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "license": "MIT", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -12345,6 +12629,15 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/tinybench": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", @@ -12389,6 +12682,12 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -12430,6 +12729,12 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==", + "license": "Unlicense" + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -12454,6 +12759,12 @@ "json5": "lib/cli.js" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -16049,8 +16360,7 @@ "@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "@jridgewell/trace-mapping": { "version": "0.3.25", @@ -16873,6 +17183,11 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" + }, "@types/json-schema": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", @@ -16913,12 +17228,6 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, - "@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true - }, "@types/react": { "version": "18.2.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", @@ -17340,6 +17649,11 @@ "integrity": "sha512-ULQu1yOoKDom7kGNg2KHeb3OJm09CbNRBZfJ/H9Mpuwia7Cydw8o0vHQEtDFyPmwYm7OIhCNEw/ILZUHzNJCQQ==", "requires": {} }, + "@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" + }, "acorn": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", @@ -17722,6 +18036,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, "requires": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -17731,6 +18046,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" @@ -18019,6 +18335,14 @@ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "dev": true }, + "copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, "core-js-compat": { "version": "3.36.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz", @@ -18084,6 +18408,14 @@ "integrity": "sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ==", "dev": true }, + "css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "requires": { + "hyphenate-style-name": "^1.0.3" + } + }, "css-mediaquery": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", @@ -18198,6 +18530,11 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, + "decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==" + }, "deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -18291,6 +18628,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -18398,6 +18736,14 @@ "is-arrayish": "^0.2.1" } }, + "error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "requires": { + "stackframe": "^1.3.4" + } + }, "es-abstract": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", @@ -18448,17 +18794,20 @@ "es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true }, "es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, "requires": { "es-errors": "^1.3.0" } @@ -19195,8 +19544,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { "version": "3.3.2", @@ -19223,6 +19571,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, "fast-uri": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", @@ -19235,6 +19588,11 @@ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true }, + "fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==" + }, "fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -19291,6 +19649,11 @@ "to-regex-range": "^5.0.1" } }, + "filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==" + }, "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -19421,6 +19784,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", @@ -19444,6 +19808,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "requires": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -19574,7 +19939,8 @@ "gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true }, "graceful-fs": { "version": "4.2.11", @@ -19631,7 +19997,8 @@ "has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true }, "has-tostringtag": { "version": "1.0.0", @@ -19646,6 +20013,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "requires": { "function-bind": "^1.1.2" } @@ -19740,6 +20108,11 @@ "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", "dev": true }, + "hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==" + }, "i18next": { "version": "23.6.0", "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.6.0.tgz", @@ -19841,6 +20214,14 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "inline-style-prefixer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", + "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", + "requires": { + "css-in-js-utils": "^3.1.0" + } + }, "internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -20282,6 +20663,11 @@ "@sideway/pinpoint": "^2.0.0" } }, + "js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -20677,7 +21063,8 @@ "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true }, "mathml-tag-names": { "version": "2.1.3", @@ -20904,6 +21291,47 @@ "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "dev": true }, + "nano-css": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz", + "integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "css-tree": "^1.1.2", + "csstype": "^3.1.2", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^7.0.1", + "rtl-css-js": "^1.16.1", + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0" + }, + "dependencies": { + "css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "stylis": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.5.tgz", + "integrity": "sha512-K7npNOKGRYuhAFFzkzMGfxFDpN6gDwf8hcMiE+uveTVbBgm93HrNP3ZDUpKqzZ4pG7TP6fmb+EMAQPjq9FqqvA==" + } + } + }, "nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -20967,7 +21395,8 @@ "object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==" + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "dev": true }, "object-keys": { "version": "1.1.1", @@ -21420,12 +21849,14 @@ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, - "qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "query-string": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.1.tgz", + "integrity": "sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==", "requires": { - "side-channel": "^1.1.0" + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" } }, "querystringify": { @@ -21497,6 +21928,33 @@ "prop-types": "^15.6.2" } }, + "react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "requires": {} + }, + "react-use": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.6.0.tgz", + "integrity": "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==", + "requires": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.6.2", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + } + }, "readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -21636,6 +22094,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "resolve": { "version": "1.22.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", @@ -21726,6 +22189,14 @@ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", "dev": true }, + "rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -21792,6 +22263,11 @@ "loose-envify": "^1.1.0" } }, + "screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==" + }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -21807,6 +22283,11 @@ "randombytes": "^2.1.0" } }, + "set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -21826,6 +22307,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, "requires": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -21838,6 +22320,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, "requires": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -21847,6 +22330,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "requires": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -21858,6 +22342,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "requires": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -21948,18 +22433,62 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "dev": true }, + "split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==" + }, "stable-hash": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", "dev": true }, + "stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "requires": { + "stackframe": "^1.3.4" + } + }, "stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "requires": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + }, + "dependencies": { + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==" + } + } + }, + "stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "requires": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -22646,6 +23175,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==" + }, "tinybench": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", @@ -22678,6 +23212,11 @@ "is-number": "^7.0.0" } }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -22710,6 +23249,11 @@ "dev": true, "requires": {} }, + "ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" + }, "tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -22733,6 +23277,11 @@ } } }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index e4aa2799..a7a47320 100644 --- a/package.json +++ b/package.json @@ -49,10 +49,11 @@ "joi": "^17.11.0", "ky": "^1.1.0", "path-to-regexp": "^8.2.0", - "qs": "^6.14.0", + "query-string": "^9.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^13.3.1", + "react-use": "^17.6.0", "runtypes": "^6.7.0", "zod": "^3.24.1" }, @@ -67,7 +68,6 @@ "@testing-library/user-event": "^14.5.2", "@types/compose-function": "^0.0.32", "@types/node": "^20.8.7", - "@types/qs": "^6.9.18", "@types/react": "^18.2.31", "@types/react-dom": "^18.2.14", "@typescript-eslint/eslint-plugin": "^6.8.0", From bf69fe39ed0b2375ef7f59d3b56241098986980f Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 21:49:34 +0400 Subject: [PATCH 25/71] refactor(auth): rewrite login form with reatom model --- .stylelintrc.json | 2 +- src/features/auth/login/index.ts | 1 - src/features/auth/login/lib/index.ts | 1 + .../auth/login/lib/use-login-model.ts | 7 + src/features/auth/login/model.ts | 122 ------------ src/features/auth/login/model/index.ts | 2 + src/features/auth/login/model/model.ts | 91 +++++++++ src/features/auth/login/model/types.ts | 11 ++ src/features/auth/login/ui.tsx | 104 ----------- src/features/auth/login/ui/index.ts | 1 + .../__snapshots__/login-form.spec.tsx.snap | 149 +++++++++++++++ .../__snapshots__/ui.spec.tsx.snap | 0 .../auth/login/ui/login-form/index.ts | 1 + .../login-form/login-form.spec.tsx} | 176 ++++++++---------- .../auth/login/ui/login-form/login-form.tsx | 136 ++++++++++++++ .../login-form/styles.module.css} | 0 .../login-form/styles.module.css.d.ts} | 0 src/pages/login/model.ts | 12 +- 18 files changed, 480 insertions(+), 336 deletions(-) create mode 100644 src/features/auth/login/lib/index.ts create mode 100644 src/features/auth/login/lib/use-login-model.ts delete mode 100644 src/features/auth/login/model.ts create mode 100644 src/features/auth/login/model/index.ts create mode 100644 src/features/auth/login/model/model.ts create mode 100644 src/features/auth/login/model/types.ts delete mode 100644 src/features/auth/login/ui.tsx create mode 100644 src/features/auth/login/ui/index.ts create mode 100644 src/features/auth/login/ui/login-form/__snapshots__/login-form.spec.tsx.snap rename src/features/auth/login/{ => ui/login-form}/__snapshots__/ui.spec.tsx.snap (100%) create mode 100644 src/features/auth/login/ui/login-form/index.ts rename src/features/auth/login/{ui.spec.tsx => ui/login-form/login-form.spec.tsx} (52%) create mode 100644 src/features/auth/login/ui/login-form/login-form.tsx rename src/features/auth/login/{ui.module.css => ui/login-form/styles.module.css} (100%) rename src/features/auth/login/{ui.module.css.d.ts => ui/login-form/styles.module.css.d.ts} (100%) diff --git a/.stylelintrc.json b/.stylelintrc.json index a942fd62..b380d2d1 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -12,7 +12,7 @@ "color-format/format": { "format": "hsl" }, - "media-feature-range-notation": "prefix", + "media-feature-range-notation": "context", "plugin/z-index-value-constraint": { "max": 10000 diff --git a/src/features/auth/login/index.ts b/src/features/auth/login/index.ts index c6ec44a5..39457aa5 100644 --- a/src/features/auth/login/index.ts +++ b/src/features/auth/login/index.ts @@ -1,2 +1 @@ -export * as loginModel from './model'; export { LoginForm } from './ui'; diff --git a/src/features/auth/login/lib/index.ts b/src/features/auth/login/lib/index.ts new file mode 100644 index 00000000..e574a88b --- /dev/null +++ b/src/features/auth/login/lib/index.ts @@ -0,0 +1 @@ +export * from './use-login-model'; diff --git a/src/features/auth/login/lib/use-login-model.ts b/src/features/auth/login/lib/use-login-model.ts new file mode 100644 index 00000000..46ad7312 --- /dev/null +++ b/src/features/auth/login/lib/use-login-model.ts @@ -0,0 +1,7 @@ +import { useMemo } from 'react'; + +import { loginModel } from '../model'; + +export const useLoginModel = () => { + return useMemo(loginModel.create, []); +}; diff --git a/src/features/auth/login/model.ts b/src/features/auth/login/model.ts deleted file mode 100644 index d6237857..00000000 --- a/src/features/auth/login/model.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { createMutation } from '@farfetched/core'; -import { runtypeContract } from '@farfetched/runtypes'; -import { createDomain, sample } from 'effector'; -import { createForm } from 'effector-forms'; -import Joi from 'joi'; -import { splitMap } from 'patronum'; - -import { authApi, authResponse, AuthResponse, LoginParams } from '@/shared/api'; -import { MAX_SHORT_LENGTH, MIN_LENGTH } from '@/shared/configs'; -import { createRuleFromSchema, isHttpErrorCode } from '@/shared/lib'; -import { sessionModel } from '@/shared/models'; -import { StandardResponse, getStandardResponse } from '@/shared/types'; - -const loginDomain = createDomain(); - -const handlerFx = loginDomain.effect< - LoginParams, - StandardResponse ->(authApi.login); - -export const mutation = createMutation< - LoginParams, - StandardResponse, - StandardResponse, - Error ->({ - effect: handlerFx, - contract: runtypeContract(getStandardResponse(authResponse)), -}); - -const schemas = { - email: Joi.string() - .email({ tlds: { allow: false, }, }) - .min(MIN_LENGTH) - .max(MAX_SHORT_LENGTH) - .required() - .messages({ - 'string.empty': 'empty', - 'string.email': 'email', - 'string.min': 'min_length', - 'string.max': 'max_length', - }), - password: Joi.string() - .min(MIN_LENGTH) - .max(MAX_SHORT_LENGTH) - .required() - .messages({ - 'string.empty': 'empty', - 'string.min': 'min_length', - 'string.max': 'max_length', - }), - rememberMe: Joi.boolean(), -}; - -export const form = createForm({ - fields: { - email: { - init: '', - rules: [createRuleFromSchema('email', schemas.email)], - }, - password: { - init: '', - rules: [createRuleFromSchema('password', schemas.password)], - }, - rememberMe: { - init: false, - rules: [createRuleFromSchema('remember', schemas.rememberMe)], - }, - }, - domain: loginDomain, -}); - -sample({ - clock: form.formValidated, - target: mutation.start, -}); - -sample({ - clock: mutation.finished.success, - fn: ({ result, }) => result.data.user, - target: sessionModel.query.start, -}); - -sample({ - clock: mutation.finished.finally, - target: form.fields.password.resetValue, -}); - -const errors = splitMap({ - source: mutation.finished.failure, - cases: { - incorrectPassword: ({ error, }) => { - if (isHttpErrorCode(error, 403)) { - return 'incorrect_password'; - } - }, - - userNotFound: ({ error, }) => { - if (isHttpErrorCode(error, 404)) { - return 'not_found'; - } - }, - }, -}); - -sample({ - clock: errors.userNotFound, - fn: (message) => ({ - rule: 'server', - errorText: message, - }), - target: form.fields.email.addError, -}); - -sample({ - clock: errors.incorrectPassword, - fn: (message) => ({ - rule: 'server', - errorText: message, - }), - target: form.fields.password.addError, -}); diff --git a/src/features/auth/login/model/index.ts b/src/features/auth/login/model/index.ts new file mode 100644 index 00000000..24f18619 --- /dev/null +++ b/src/features/auth/login/model/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * as loginModel from './model'; diff --git a/src/features/auth/login/model/model.ts b/src/features/auth/login/model/model.ts new file mode 100644 index 00000000..788b1eff --- /dev/null +++ b/src/features/auth/login/model/model.ts @@ -0,0 +1,91 @@ +import { atom, onDisconnect } from '@reatom/framework'; +import zod from 'zod'; + +import { reatomZodForm } from '@reatom/form'; + +import { authApi } from '@/shared/api'; +import { MAX_SHORT_LENGTH, MIN_LENGTH } from '@/shared/configs'; +import { + constructName, + createSingletonFactory, + isHttpErrorCode +} from '@/shared/lib'; + +import { LoginModel } from './types'; + +const schema = zod.object({ + email: zod.string().email('email').nonempty('empty'), + password: zod + .string() + .min(MIN_LENGTH, 'min_length') + .max(MAX_SHORT_LENGTH, 'max_length') + .nonempty('empty'), + rememberMe: zod.boolean(), +}); + +const modelName = 'login-form'; + +export const create = createSingletonFactory( + (): LoginModel => { + const form = reatomZodForm( + { + email: '', + password: '', + rememberMe: false as boolean, + }, + { + name: modelName, + resetOnSubmit: false, + schema, + onSubmit: async (ctx, state) => { + try { + // @todo Add logic to update session + await authApi.login(state); + } catch (error) { + if (isHttpErrorCode(error, 403)) { + form.fields.password.validation.merge(ctx, { + error: 'incorrect_password', + }); + } else if (isHttpErrorCode(error, 404)) { + form.fields.email.validation.merge(ctx, { + error: 'not_found', + }); + } + + throw error; + } finally { + // Save and set validation state again, + // because field reset is resettings validation state also + const validation = ctx.get(form.fields.password.validation); + + form.fields.password.reset(ctx); + form.fields.password.validation(ctx, validation); + } + }, + } + ); + + const { submit, } = form; + const { statusesAtom, } = submit; + const { email, password, rememberMe, } = form.fields; + + const submittingAtom = atom( + (ctx) => ctx.spy(statusesAtom).isPending, + constructName(modelName, 'submittingAtom') + ); + + return { + submit, + submittingAtom, + email, + rememberMe, + password, + }; + }, + { + key: modelName, + hooks: { + staleOn: (result, stale) => onDisconnect(result.email, stale), + }, + } +); diff --git a/src/features/auth/login/model/types.ts b/src/features/auth/login/model/types.ts new file mode 100644 index 00000000..91707042 --- /dev/null +++ b/src/features/auth/login/model/types.ts @@ -0,0 +1,11 @@ +import { Action, Atom } from '@reatom/framework'; + +import { FieldAtom } from '@reatom/form'; + +export interface LoginModel { + readonly submit: Action; + readonly email: FieldAtom; + readonly password: FieldAtom; + readonly rememberMe: FieldAtom; + readonly submittingAtom: Atom; +} diff --git a/src/features/auth/login/ui.tsx b/src/features/auth/login/ui.tsx deleted file mode 100644 index 6d43b9b0..00000000 --- a/src/features/auth/login/ui.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Button } from '@mui/material'; -import cn from 'classnames'; -import { useUnit } from 'effector-react'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { MAX_SHORT_LENGTH, MIN_LENGTH } from '@/shared/configs'; -import { usePreventDefault } from '@/shared/lib'; -import { CommonProps } from '@/shared/types'; -import { Checkbox, Field, Form, PasswordField } from '@/shared/ui'; - -import { form } from './model'; -import styles from './ui.module.css'; - -export type LoginFormProps = CommonProps; - -export const LoginForm: React.FC = (props) => { - const { className, } = props; - const { t, } = useTranslation('login'); - const loginText = t('login_form.submit'); - const formTitleText = t('login_form.title'); - const submit = useUnit(form.submit); - - const onSubmit = usePreventDefault(submit); - - return ( -
    - - - - - - ); -}; - -const Email: React.FC = () => { - const { t, } = useTranslation('login'); - const email = useUnit(form.fields.email); - const { errorText, } = email; - const label = t('login_form.fields.email'); - const error = t(`login_form.errors.email.${errorText}`, { - min_symbols_count: MIN_LENGTH, - max_symbols_count: MAX_SHORT_LENGTH, - }); - const errorHelperText = email.isValid ? null : error; - - return ( - - ); -}; - -const Password: React.FC = () => { - const { t, } = useTranslation('login'); - const password = useUnit(form.fields.password); - const { errorText, } = password; - - const label = t('login_form.fields.password'); - const error = t(`login_form.errors.password.${errorText}`, { - min_symbols_count: MIN_LENGTH, - max_symbols_count: MAX_SHORT_LENGTH, - }); - const errorHelperText = password.isValid ? null : error; - - return ( - - ); -}; - -const RememberMe: React.FC = () => { - const { t, } = useTranslation('login'); - const label = t('login_form.fields.remember_me'); - - const rememberMe = useUnit(form.fields.rememberMe); - - return ( - - ); -}; diff --git a/src/features/auth/login/ui/index.ts b/src/features/auth/login/ui/index.ts new file mode 100644 index 00000000..ab8f2b0f --- /dev/null +++ b/src/features/auth/login/ui/index.ts @@ -0,0 +1 @@ +export * from './login-form'; diff --git a/src/features/auth/login/ui/login-form/__snapshots__/login-form.spec.tsx.snap b/src/features/auth/login/ui/login-form/__snapshots__/login-form.spec.tsx.snap new file mode 100644 index 00000000..88b54202 --- /dev/null +++ b/src/features/auth/login/ui/login-form/__snapshots__/login-form.spec.tsx.snap @@ -0,0 +1,149 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`features/auth/login/ui/login-form/login-form.tsx > should render form, 3 inputs and button 1`] = ` +
    +
    + +
    + + +
    +
    +
    + +
    + +
    + +
    + +
    +
    + + +
    +`; diff --git a/src/features/auth/login/__snapshots__/ui.spec.tsx.snap b/src/features/auth/login/ui/login-form/__snapshots__/ui.spec.tsx.snap similarity index 100% rename from src/features/auth/login/__snapshots__/ui.spec.tsx.snap rename to src/features/auth/login/ui/login-form/__snapshots__/ui.spec.tsx.snap diff --git a/src/features/auth/login/ui/login-form/index.ts b/src/features/auth/login/ui/login-form/index.ts new file mode 100644 index 00000000..ab8f2b0f --- /dev/null +++ b/src/features/auth/login/ui/login-form/index.ts @@ -0,0 +1 @@ +export * from './login-form'; diff --git a/src/features/auth/login/ui.spec.tsx b/src/features/auth/login/ui/login-form/login-form.spec.tsx similarity index 52% rename from src/features/auth/login/ui.spec.tsx rename to src/features/auth/login/ui/login-form/login-form.spec.tsx index 05ef6760..f485c136 100644 --- a/src/features/auth/login/ui.spec.tsx +++ b/src/features/auth/login/ui/login-form/login-form.spec.tsx @@ -1,18 +1,20 @@ /* eslint-disable import/no-extraneous-dependencies */ import { describe, expect, test } from 'vitest'; -import { LoginForm, LoginFormProps } from './ui'; - import { handlers, server, RenderResult, fireEvent, render, - waitFor + waitFor, + act } from '~/test-utils'; -describe('features/auth/login/ui', () => { +import { LoginForm, LoginFormProps } from './login-form'; + + +describe('features/auth/login/ui/login-form/login-form.tsx', () => { const values = { email: 'email@example.com', password: 'password', @@ -25,16 +27,23 @@ describe('features/auth/login/ui', () => { wrapper = render(); }; - const foundForm = () => wrapper.getByRole('form'); - const foundItems = () => ({ - email: wrapper.getByRole('textbox', { name: 'login_form.fields.email', }), - password: wrapper.getByLabelText('login_form.fields.password'), - rememberMe: wrapper.getByRole('checkbox', { + const findForm = () => wrapper.getByRole('form') as HTMLFormElement; + const findEmailField = () => + wrapper.getByRole('textbox', { + name: 'login_form.fields.email', + }) as HTMLInputElement; + const findPasswordField = () => + wrapper.getByLabelText('login_form.fields.password') as HTMLInputElement; + const findRememberCheckbox = () => + wrapper.getByRole('checkbox', { name: 'login_form.fields.remember_me', - }), - submit: wrapper.getByRole('button', { name: 'login_form.submit', }), - }); - const fillFields = async (items, values) => { + }) as HTMLInputElement; + const findSubmitButton = () => + wrapper.getByRole('button', { + name: 'login_form.submit', + }) as HTMLButtonElement; + + const setValues = async (items, values) => { fireEvent.input(items.email, { target: { value: values.email, }, }); @@ -46,29 +55,32 @@ describe('features/auth/login/ui', () => { }); }; - test('should render form, 3 inputs and button', () => { - createComponent(); + test('should render form, 3 inputs and button', async () => { + await act(async () => createComponent()); - expect(foundForm()).toMatchSnapshot(); + expect(findForm()).toMatchSnapshot(); }); test('should be able to click on button if fields are empty', async () => { - createComponent(); + await act(async () => createComponent()); - const { submit, } = foundItems(); + const submit = findSubmitButton(); expect(submit).not.toHaveAttribute('disabled', true); - fireEvent.click(submit); + await wrapper.user.click(submit); }); test('should send login query with data from fields', async () => { - createComponent(); + await act(async () => createComponent()); - const { submit, email, password, rememberMe, } = foundItems(); - fillFields({ email, password, rememberMe, }, values); + const email = findEmailField(); + const password = findPasswordField(); + const rememberMe = findRememberCheckbox(); - fireEvent.click(submit); + await setValues({ email, password, rememberMe, }, values); + + await wrapper.user.click(findSubmitButton()); await waitFor(() => { expect(email.value).toBe(values.email); @@ -80,12 +92,15 @@ describe('features/auth/login/ui', () => { describe('validation', () => { describe('email field', () => { test('empty field', async () => { - createComponent(); + await act(async () => createComponent()); + + const email = findEmailField(); + const password = findPasswordField(); + const rememberMe = findRememberCheckbox(); - const { submit, email, password, rememberMe, } = foundItems(); - fillFields({ email, password, rememberMe, }, { ...values, email: '', }); + setValues({ email, password, rememberMe, }, { ...values, email: '', }); - fireEvent.click(submit); + await wrapper.user.click(findSubmitButton()); await waitFor(() => { const element = wrapper.getByText('login_form.errors.email.empty'); @@ -95,15 +110,17 @@ describe('features/auth/login/ui', () => { }); test('invalid pattern', async () => { - createComponent(); + await act(async () => createComponent()); - const { submit, email, password, rememberMe, } = foundItems(); - fillFields( + const email = findEmailField(); + const password = findPasswordField(); + const rememberMe = findRememberCheckbox(); + setValues( { email, password, rememberMe, }, { ...values, email: 'email.com', } ); - fireEvent.click(submit); + await wrapper.user.click(findSubmitButton()); await waitFor(() => { const element = wrapper.getByText('login_form.errors.email.email'); @@ -112,62 +129,20 @@ describe('features/auth/login/ui', () => { }); }); - test('too short email', async () => { - createComponent(); - - const { submit, email, password, rememberMe, } = foundItems(); - fillFields( - { email, password, rememberMe, }, - { ...values, email: 'e@g.c', } - ); - - fireEvent.click(submit); - - await waitFor(() => { - const element = wrapper.getByText( - 'login_form.errors.email.min_length' - ); - - expect(element).toBeInTheDocument(); - }); - }); - - test('too long email', async () => { - createComponent(); - - const { submit, email, password, rememberMe, } = foundItems(); - fillFields( - { email, password, rememberMe, }, - { - ...values, - email: - 'asdfasdfasdasdfasdfasdfasdfasdfasdffasdfasdfeasdfasdf1123@gmail.com', - } - ); - - fireEvent.click(submit); - - await waitFor(() => { - const element = wrapper.getByText( - 'login_form.errors.email.max_length' - ); - - expect(element).toBeInTheDocument(); - }); - }); - test('there is not user with this email', async () => { server.use(handlers.auth.error.login.notFound); - createComponent(); + await act(async () => createComponent()); - const { submit, email, password, rememberMe, } = foundItems(); - fillFields( + const email = findEmailField(); + const password = findPasswordField(); + const rememberMe = findRememberCheckbox(); + setValues( { email, password, rememberMe, }, { ...values, email: 'asd@gmail.com', } ); - fireEvent.click(submit); + await wrapper.user.click(findSubmitButton()); await waitFor(() => { const element = wrapper.getByText( @@ -180,15 +155,14 @@ describe('features/auth/login/ui', () => { }); describe('password field', () => { test('empty field', async () => { - createComponent(); + await act(async () => createComponent()); - const { submit, email, password, rememberMe, } = foundItems(); - fillFields( - { email, password, rememberMe, }, - { ...values, password: '', } - ); + const email = findEmailField(); + const password = findPasswordField(); + const rememberMe = findRememberCheckbox(); + setValues({ email, password, rememberMe, }, { ...values, password: '', }); - fireEvent.click(submit); + await wrapper.user.click(findSubmitButton()); await waitFor(() => { const element = wrapper.getByText('login_form.errors.password.empty'); @@ -198,15 +172,17 @@ describe('features/auth/login/ui', () => { }); test('too short password', async () => { - createComponent(); + await act(async () => createComponent()); - const { submit, email, password, rememberMe, } = foundItems(); - fillFields( + const email = findEmailField(); + const password = findPasswordField(); + const rememberMe = findRememberCheckbox(); + setValues( { email, password, rememberMe, }, { ...values, password: 'e@g.c', } ); - fireEvent.click(submit); + await wrapper.user.click(findSubmitButton()); await waitFor(() => { const element = wrapper.getByText( @@ -218,10 +194,12 @@ describe('features/auth/login/ui', () => { }); test('too long password', async () => { - createComponent(); + await act(async () => createComponent()); - const { submit, email, password, rememberMe, } = foundItems(); - fillFields( + const email = findEmailField(); + const password = findPasswordField(); + const rememberMe = findRememberCheckbox(); + setValues( { email, password, rememberMe, }, { ...values, @@ -230,7 +208,7 @@ describe('features/auth/login/ui', () => { } ); - fireEvent.click(submit); + await wrapper.user.click(findSubmitButton()); await waitFor(() => { const element = wrapper.getByText( @@ -244,15 +222,17 @@ describe('features/auth/login/ui', () => { test('incorrect password', async () => { server.use(handlers.auth.error.login.forbidden); - createComponent(); + await act(async () => createComponent()); - const { submit, email, password, rememberMe, } = foundItems(); - fillFields( + const email = findEmailField(); + const password = findPasswordField(); + const rememberMe = findRememberCheckbox(); + setValues( { email, password, rememberMe, }, { ...values, password: 'asd@gmail.com', } ); - fireEvent.click(submit); + await wrapper.user.click(findSubmitButton()); await waitFor(() => { const element = wrapper.getByText( diff --git a/src/features/auth/login/ui/login-form/login-form.tsx b/src/features/auth/login/ui/login-form/login-form.tsx new file mode 100644 index 00000000..798b03b6 --- /dev/null +++ b/src/features/auth/login/ui/login-form/login-form.tsx @@ -0,0 +1,136 @@ +import { Button } from '@mui/material'; +import { useAction, useAtom } from '@reatom/npm-react'; +import cn from 'classnames'; +import { FC, memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { FieldAtom } from '@reatom/form'; + +import { MAX_SHORT_LENGTH, MIN_LENGTH } from '@/shared/configs'; +import { usePreventDefault } from '@/shared/lib'; +import { CommonProps } from '@/shared/types'; +import { Checkbox, Field, Form, PasswordField } from '@/shared/ui'; + +import { useLoginModel } from '../../lib'; + +import styles from './styles.module.css'; + + +export interface LoginFormProps extends CommonProps {} + +export const LoginForm: FC = memo((props) => { + const { className, } = props; + + const { t, } = useTranslation('login'); + + const model = useLoginModel(); + const submit = useAction(model.submit); + const onSubmit = usePreventDefault(submit); + + const submitT = t('login_form.submit'); + const titleT = t('login_form.title'); + + return ( +
    + + + + + + ); +}); + +interface FieldProps { + readonly field: FieldAtom; +} + +const Email: FC = memo((props) => { + const { field, } = props; + + const [value] = useAtom(field.value); + const [error] = useAtom((ctx) => ctx.spy(field.validation).error); + const change = useAction(field.change); + const focus = useAction(field.focus.in); + const blur = useAction(field.focus.out); + const { t, } = useTranslation('login', { keyPrefix: 'login_form', }); + + const labelT = t('fields.email'); + const errorT = t(`errors.email.${error}`, { + min_symbols_count: MIN_LENGTH, + max_symbols_count: MAX_SHORT_LENGTH, + }); + + const isError = !!error; + const errorHelperText = isError ? errorT : null; + + return ( + + ); +}); + +const Password: FC = memo((props) => { + const { field, } = props; + + const [value] = useAtom(field.value); + const [error] = useAtom((ctx) => ctx.spy(field.validation).error); + const change = useAction(field.change); + const focus = useAction(field.focus.in); + const blur = useAction(field.focus.out); + const { t, } = useTranslation('login', { keyPrefix: 'login_form', }); + + const labelT = t('fields.password'); + const errorT = t(`errors.password.${error}`, { + min_symbols_count: MIN_LENGTH, + max_symbols_count: MAX_SHORT_LENGTH, + }); + + const isError = !!error; + const errorHelperText = isError ? errorT : null; + + return ( + + ); +}); + +const RememberMe: FC = memo((props) => { + const { field, } = props; + + const [value] = useAtom(field.value); + const change = useAction(field.change); + const { t, } = useTranslation('login', { keyPrefix: 'login_form', }); + + const labelT = t('fields.remember_me'); + + return ( + + ); +}); diff --git a/src/features/auth/login/ui.module.css b/src/features/auth/login/ui/login-form/styles.module.css similarity index 100% rename from src/features/auth/login/ui.module.css rename to src/features/auth/login/ui/login-form/styles.module.css diff --git a/src/features/auth/login/ui.module.css.d.ts b/src/features/auth/login/ui/login-form/styles.module.css.d.ts similarity index 100% rename from src/features/auth/login/ui.module.css.d.ts rename to src/features/auth/login/ui/login-form/styles.module.css.d.ts diff --git a/src/pages/login/model.ts b/src/pages/login/model.ts index fb160f8c..98c36ec7 100644 --- a/src/pages/login/model.ts +++ b/src/pages/login/model.ts @@ -1,16 +1,8 @@ -import { sample } from 'effector'; - -import { loginModel } from '@/features/auth'; - -import { routes } from '@/shared/configs'; import { sessionModel } from '@/shared/models'; +const routes = {}; + export const currentRoute = routes.login; export const anonymousRoute = sessionModel.chainAnonymous(currentRoute, { otherwise: routes.rooms.base.open, }); - -sample({ - clock: anonymousRoute.closed, - target: loginModel.form.reset, -}); From 0d1716e2255a934dfff2435cee729952e587b40e Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 21:50:17 +0400 Subject: [PATCH 26/71] fix(auth): fix registration form model with cache --- src/features/auth/registration/index.ts | 1 - src/features/auth/registration/model/model.ts | 115 ++++++++++-------- src/features/auth/registration/ui/form.tsx | 5 +- 3 files changed, 67 insertions(+), 54 deletions(-) diff --git a/src/features/auth/registration/index.ts b/src/features/auth/registration/index.ts index 501d9d34..db0afcf3 100644 --- a/src/features/auth/registration/index.ts +++ b/src/features/auth/registration/index.ts @@ -1,2 +1 @@ -export * as registrationModel from './model/model'; export { RegistrationForm } from './ui'; diff --git a/src/features/auth/registration/model/model.ts b/src/features/auth/registration/model/model.ts index a116272b..3eaa0837 100644 --- a/src/features/auth/registration/model/model.ts +++ b/src/features/auth/registration/model/model.ts @@ -1,77 +1,92 @@ +import { atom, onDisconnect } from '@reatom/framework'; +import zod from 'zod'; + import { reatomZodForm } from '@reatom/form'; -import { atom } from '@reatom/framework'; -import z from 'zod'; import { authApi } from '@/shared/api'; import { MIN_LENGTH, MAX_SHORT_LENGTH } from '@/shared/configs'; -import { constructName, isHttpErrorCode } from '@/shared/lib'; +import { + constructName, + createSingletonFactory, + isHttpErrorCode +} from '@/shared/lib'; import { RegistrationModel } from './types'; -const schema = z +const schema = zod .object({ - email: z.string().email('email').nonempty('empty'), - username: z + email: zod.string().email('email').nonempty('empty'), + username: zod .string() .min(MIN_LENGTH, 'min_length') .max(MAX_SHORT_LENGTH, 'max_length') .nonempty('empty'), - password: z + password: zod .string() .min(MIN_LENGTH, 'min_length') .max(MAX_SHORT_LENGTH, 'max_length') .nonempty('empty'), - repeatPassword: z.string(), + repeatPassword: zod.string(), }) .refine((data) => data.password === data.repeatPassword, { message: 'equal', path: ['repeatPassword'], }); -export const create = (): RegistrationModel => { - const form = reatomZodForm( - { - username: '', - email: '', - password: '', - repeatPassword: '', - }, - { - name: `registration-form`, - resetOnSubmit: false, - schema, - onSubmit: async (ctx, state) => { - try { - await authApi.registration(state); - } catch (error) { - if (isHttpErrorCode(error, 409)) { - form.fields.email.validation.merge(ctx, { error: 'exists', }); - } +const modelName = 'registration-form'; - throw error; - } finally { - form.fields.password.reset(ctx); - form.fields.repeatPassword.reset(ctx); - } +export const create = createSingletonFactory( + (): RegistrationModel => { + const form = reatomZodForm( + { + username: '', + email: '', + password: '', + repeatPassword: '', }, - } - ); + { + name: modelName, + resetOnSubmit: false, + schema, + onSubmit: async (ctx, state) => { + try { + await authApi.registration(state); + } catch (error) { + if (isHttpErrorCode(error, 409)) { + form.fields.email.validation.merge(ctx, { error: 'exists', }); + } - const { submit, } = form; - const { statusesAtom, } = submit; - const { email, password, repeatPassword, username, } = form.fields; + throw error; + } finally { + form.fields.password.reset(ctx); + form.fields.repeatPassword.reset(ctx); + } + }, + } + ); + + const { submit, } = form; + const { statusesAtom, } = submit; + const { email, password, repeatPassword, username, } = form.fields; - const submittingAtom = atom( - (ctx) => ctx.spy(statusesAtom).isPending, - constructName('registration-form', 'submittingAtom') - ); + const submittingAtom = atom( + (ctx) => ctx.spy(statusesAtom).isPending, + constructName(modelName, 'submittingAtom') + ); - return { - submit, - submittingAtom, - email, - password, - repeatPassword, - username, - }; -}; + return { + submit, + submittingAtom, + email, + password, + repeatPassword, + username, + }; + }, + { + key: modelName, + hooks: { + staleOn: (result, stale) => onDisconnect(result.username, stale), + }, + } +); diff --git a/src/features/auth/registration/ui/form.tsx b/src/features/auth/registration/ui/form.tsx index bf179558..b9d83de7 100644 --- a/src/features/auth/registration/ui/form.tsx +++ b/src/features/auth/registration/ui/form.tsx @@ -1,11 +1,12 @@ /* eslint-disable sonarjs/no-duplicate-string */ import { Button } from '@mui/material'; -import { FieldAtom } from '@reatom/form'; import { useAction, useAtom } from '@reatom/npm-react'; import cn from 'classnames'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; +import { FieldAtom } from '@reatom/form'; + import { MIN_LENGTH, MAX_SHORT_LENGTH } from '@/shared/configs'; import { usePreventDefault } from '@/shared/lib'; import { CommonProps } from '@/shared/types'; @@ -109,8 +110,6 @@ const Username: React.FC = (props) => { max_symbols_count: MAX_SHORT_LENGTH, }); - console.log(useAtom(fieldAtom.validation)[0]); - const isError = !!errorText; const errorHelperText = isError ? error : null; From df9f63236f4b694431f0b87be30bb86d529e2b86 Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 21:59:26 +0400 Subject: [PATCH 27/71] refactor(auth): rename registraion form component and add subdir --- src/features/auth/registration/model/types.ts | 3 ++- .../__snapshots__/registration-form.spec.tsx.snap} | 4 ++-- .../auth/registration/ui/registration-form/index.ts | 1 + .../registration-form.spec.tsx} | 8 ++++---- .../{form.tsx => registration-form/registration-form.tsx} | 4 ++-- .../styles.module.css} | 0 .../styles.module.css.d.ts} | 0 7 files changed, 11 insertions(+), 9 deletions(-) rename src/features/auth/registration/ui/{__snapshots__/form.spec.tsx.snap => registration-form/__snapshots__/registration-form.spec.tsx.snap} (97%) create mode 100644 src/features/auth/registration/ui/registration-form/index.ts rename src/features/auth/registration/ui/{form.spec.tsx => registration-form/registration-form.spec.tsx} (97%) rename src/features/auth/registration/ui/{form.tsx => registration-form/registration-form.tsx} (98%) rename src/features/auth/registration/ui/{form.module.css => registration-form/styles.module.css} (100%) rename src/features/auth/registration/ui/{form.module.css.d.ts => registration-form/styles.module.css.d.ts} (100%) diff --git a/src/features/auth/registration/model/types.ts b/src/features/auth/registration/model/types.ts index d2f4e46d..89023a50 100644 --- a/src/features/auth/registration/model/types.ts +++ b/src/features/auth/registration/model/types.ts @@ -1,6 +1,7 @@ -import { FieldAtom } from '@reatom/form'; import { AsyncAction, Atom } from '@reatom/framework'; +import { FieldAtom } from '@reatom/form'; + export interface RegistrationModel { readonly submit: AsyncAction<[], void>; readonly submittingAtom: Atom; diff --git a/src/features/auth/registration/ui/__snapshots__/form.spec.tsx.snap b/src/features/auth/registration/ui/registration-form/__snapshots__/registration-form.spec.tsx.snap similarity index 97% rename from src/features/auth/registration/ui/__snapshots__/form.spec.tsx.snap rename to src/features/auth/registration/ui/registration-form/__snapshots__/registration-form.spec.tsx.snap index aedb5ef7..1be5468c 100644 --- a/src/features/auth/registration/ui/__snapshots__/form.spec.tsx.snap +++ b/src/features/auth/registration/ui/registration-form/__snapshots__/registration-form.spec.tsx.snap @@ -1,9 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`features/auth/registration/ui/form.tsx > should render form, 4 inputs and button 1`] = ` +exports[`features/auth/registration/ui/registration-form/registration-form.tsx > should render form, 4 inputs and button 1`] = `
    { +import { RegistrationForm, RegistrationFormProps } from './registration-form'; + + +describe('features/auth/registration/ui/registration-form/registration-form.tsx', () => { const values = { email: 'email@example.com', username: 'username', diff --git a/src/features/auth/registration/ui/form.tsx b/src/features/auth/registration/ui/registration-form/registration-form.tsx similarity index 98% rename from src/features/auth/registration/ui/form.tsx rename to src/features/auth/registration/ui/registration-form/registration-form.tsx index b9d83de7..ea2efa43 100644 --- a/src/features/auth/registration/ui/form.tsx +++ b/src/features/auth/registration/ui/registration-form/registration-form.tsx @@ -12,9 +12,9 @@ import { usePreventDefault } from '@/shared/lib'; import { CommonProps } from '@/shared/types'; import { Field, Form, PasswordField } from '@/shared/ui'; -import { useRegistrationModel } from '../lib'; +import { useRegistrationModel } from '../../lib'; -import styles from './form.module.css'; +import styles from './styles.module.css'; export interface RegistrationFormProps extends CommonProps {} diff --git a/src/features/auth/registration/ui/form.module.css b/src/features/auth/registration/ui/registration-form/styles.module.css similarity index 100% rename from src/features/auth/registration/ui/form.module.css rename to src/features/auth/registration/ui/registration-form/styles.module.css diff --git a/src/features/auth/registration/ui/form.module.css.d.ts b/src/features/auth/registration/ui/registration-form/styles.module.css.d.ts similarity index 100% rename from src/features/auth/registration/ui/form.module.css.d.ts rename to src/features/auth/registration/ui/registration-form/styles.module.css.d.ts From 52882cde32a41712d57dc1af50d75663c7f4fa7d Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 22:01:26 +0400 Subject: [PATCH 28/71] refactor(auth): remove extra snapshot from login form --- .../login-form/__snapshots__/ui.spec.tsx.snap | 149 ------------------ 1 file changed, 149 deletions(-) delete mode 100644 src/features/auth/login/ui/login-form/__snapshots__/ui.spec.tsx.snap diff --git a/src/features/auth/login/ui/login-form/__snapshots__/ui.spec.tsx.snap b/src/features/auth/login/ui/login-form/__snapshots__/ui.spec.tsx.snap deleted file mode 100644 index 8c85d191..00000000 --- a/src/features/auth/login/ui/login-form/__snapshots__/ui.spec.tsx.snap +++ /dev/null @@ -1,149 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`features/auth/login/ui > should render form, 3 inputs and button 1`] = ` - -
    - -
    - - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -`; From b3af40f54e5d60babd6ed57e4201fa77aba8031c Mon Sep 17 00:00:00 2001 From: Bricks666 Date: Sun, 26 Jan 2025 22:28:49 +0400 Subject: [PATCH 29/71] refactor(auth): rewrite logout logic. Remove profile menu and use just a logout button --- .../__snapshots__/profile-menu.spec.tsx.snap | 165 ------------------ src/features/auth/logout/index.ts | 3 +- src/features/auth/logout/model.ts | 30 ---- src/features/auth/logout/model/index.ts | 1 + src/features/auth/logout/model/model.ts | 8 + .../auth/logout/profile-menu.spec.tsx | 119 ------------- src/features/auth/logout/profile-menu.tsx | 73 -------- src/features/auth/logout/ui/index.ts | 1 + .../__snapshots__/logout-button.spec.tsx.snap | 26 +++ .../auth/logout/ui/logout-button/index.ts | 1 + .../ui/logout-button/logout-button.spec.tsx | 41 +++++ .../logout/ui/logout-button/logout-button.tsx | 29 +++ .../page/ui/main-header/main-header.tsx | 4 +- 13 files changed, 110 insertions(+), 391 deletions(-) delete mode 100644 src/features/auth/logout/__snapshots__/profile-menu.spec.tsx.snap delete mode 100644 src/features/auth/logout/model.ts create mode 100644 src/features/auth/logout/model/index.ts create mode 100644 src/features/auth/logout/model/model.ts delete mode 100644 src/features/auth/logout/profile-menu.spec.tsx delete mode 100644 src/features/auth/logout/profile-menu.tsx create mode 100644 src/features/auth/logout/ui/index.ts create mode 100644 src/features/auth/logout/ui/logout-button/__snapshots__/logout-button.spec.tsx.snap create mode 100644 src/features/auth/logout/ui/logout-button/index.ts create mode 100644 src/features/auth/logout/ui/logout-button/logout-button.spec.tsx create mode 100644 src/features/auth/logout/ui/logout-button/logout-button.tsx diff --git a/src/features/auth/logout/__snapshots__/profile-menu.spec.tsx.snap b/src/features/auth/logout/__snapshots__/profile-menu.spec.tsx.snap deleted file mode 100644 index 444f7259..00000000 --- a/src/features/auth/logout/__snapshots__/profile-menu.spec.tsx.snap +++ /dev/null @@ -1,165 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`features/auth/logout/profile-menu > should render button when menu is closed > closed 1`] = ` - -
    -
    - -
    -
    - -`; - -exports[`features/auth/logout/profile-menu > should render button when menu is not opened > opened 1`] = ` - - -