From 0d232b7ecdd9866fb13695b7f5c51a677cdf07a4 Mon Sep 17 00:00:00 2001 From: Orka Arnest CRUZE Date: Mon, 20 Nov 2023 14:50:04 +0100 Subject: [PATCH] feat: introduit auth-store zustand --- assets/App.tsx | 25 +++++++---- assets/components/Layout/AppHeader.tsx | 4 +- assets/components/Layout/AppLayout.tsx | 8 +++- assets/components/Utils/AlertsContainer.tsx | 22 ++++++++++ .../components/Utils/SessionExpiredAlert.tsx | 30 +++++++++++++ assets/contexts/UserContext.tsx | 41 ------------------ assets/hooks/useDatastoreList.tsx | 6 +-- assets/hooks/useUser.tsx | 11 ----- assets/modules/jsonFetch.ts | 10 +++++ assets/pages/Home.tsx | 25 +++++++++++ assets/pages/communities/CommunityList.tsx | 31 ++++++------- assets/pages/contact/Contact.tsx | 4 +- assets/pages/dashboard/DashboardPro.tsx | 4 +- assets/pages/users/Me.tsx | 4 +- assets/router/RouterRenderer.tsx | 4 +- assets/stores/AlertsStore.tsx | 43 +++++++++++++++++++ assets/stores/AuthStore.tsx | 22 ++++++++++ assets/types/app.ts | 13 ++++++ config/packages/web_profiler.yaml | 4 +- package.json | 3 +- src/Constants/EntrepotApi/StoredDataTags.php | 1 + src/Controller/Api/DatasheetController.php | 3 +- src/Controller/Api/PyramidController.php | 1 + src/Controller/Api/WfsController.php | 1 + src/Listener/InternalApiSubscriber.php | 15 +++++-- yarn.lock | 12 ++++++ 26 files changed, 249 insertions(+), 98 deletions(-) create mode 100644 assets/components/Utils/AlertsContainer.tsx create mode 100644 assets/components/Utils/SessionExpiredAlert.tsx delete mode 100644 assets/contexts/UserContext.tsx delete mode 100644 assets/hooks/useUser.tsx create mode 100644 assets/stores/AlertsStore.tsx create mode 100644 assets/stores/AuthStore.tsx diff --git a/assets/App.tsx b/assets/App.tsx index 0ef5b9eb..c55ce427 100644 --- a/assets/App.tsx +++ b/assets/App.tsx @@ -1,24 +1,31 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { FC } from "react"; + import ErrorBoundary from "./components/Utils/ErrorBoundary"; -import { UserContextProvider } from "./contexts/UserContext"; import RouterRenderer from "./router/RouterRenderer"; import { RouteProvider } from "./router/router"; -import { FC } from "react"; +import { useAuthStore } from "./stores/AuthStore"; + +const userFromTwig = (document.getElementById("user") as HTMLDivElement)?.dataset?.user ?? null; +const user = userFromTwig === null ? null : JSON.parse(userFromTwig); +useAuthStore.getState().setUser(user); const queryClient = new QueryClient(); const App: FC = () => { + const authStore = useAuthStore(); + + console.log("authStore.sessionExpired", authStore.sessionExpired); + return ( - - - - - - - + + + + + ); }; diff --git a/assets/components/Layout/AppHeader.tsx b/assets/components/Layout/AppHeader.tsx index 13becc3f..e2d11442 100644 --- a/assets/components/Layout/AppHeader.tsx +++ b/assets/components/Layout/AppHeader.tsx @@ -2,10 +2,10 @@ import Header, { HeaderProps } from "@codegouvfr/react-dsfr/Header"; import { MainNavigationProps } from "@codegouvfr/react-dsfr/MainNavigation/MainNavigation"; import { FC, ReactNode } from "react"; -import useUser from "../../hooks/useUser"; // import { useLang } from "../../i18n/i18n"; import SymfonyRouting from "../../modules/Routing"; import { routes } from "../../router/router"; +import { useAuthStore } from "../../stores/AuthStore"; // import LanguageSelector from "../Utils/LanguageSelector"; import "../../sass/components/header.scss"; @@ -16,7 +16,7 @@ type AppHeaderProps = { navItems?: NavigationProps; }; const AppHeader: FC = ({ navItems = [] }) => { - const { user } = useUser(); + const { user } = useAuthStore(); // const { lang, setLang } = useLang(); const quickAccessItems: (HeaderProps.QuickAccessItem | ReactNode)[] = []; diff --git a/assets/components/Layout/AppLayout.tsx b/assets/components/Layout/AppLayout.tsx index dee64a7e..a29e34c2 100644 --- a/assets/components/Layout/AppLayout.tsx +++ b/assets/components/Layout/AppLayout.tsx @@ -8,8 +8,10 @@ import { defaultNavItems } from "../../config/navItems"; import useDocumentTitle from "../../hooks/useDocumentTitle"; import { useTranslation } from "../../i18n/i18n"; import Translator from "../../modules/Translator"; +import AlertsContainer from "../Utils/AlertsContainer"; import AppFooter from "./AppFooter"; import AppHeader, { NavigationProps } from "./AppHeader"; +import SessionExpiredAlert from "../Utils/SessionExpiredAlert"; type AppLayoutProps = { navItems?: NavigationProps; @@ -50,7 +52,11 @@ const AppLayout: FC> = ({ children, navItems, {/* doit être le premier élément atteignable après le lien d'évitement (Accessibilité) : https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/bandeau-d-information-importante */} {infoBannerMsg && } -
{children}
+
+ + + {children} +
diff --git a/assets/components/Utils/AlertsContainer.tsx b/assets/components/Utils/AlertsContainer.tsx new file mode 100644 index 00000000..7de535de --- /dev/null +++ b/assets/components/Utils/AlertsContainer.tsx @@ -0,0 +1,22 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Alert from "@codegouvfr/react-dsfr/Alert"; +import { FC } from "react"; + +import { useAlertsStore } from "../../stores/AlertsStore"; + +/** + * Composant qui rend les Alertes dans la vue + */ +const AlertsContainer: FC = () => { + const alerts = useAlertsStore((state) => state.alerts); + + return ( +
+ {alerts.map((alert) => ( + + ))} +
+ ); +}; + +export default AlertsContainer; diff --git a/assets/components/Utils/SessionExpiredAlert.tsx b/assets/components/Utils/SessionExpiredAlert.tsx new file mode 100644 index 00000000..d98f1bee --- /dev/null +++ b/assets/components/Utils/SessionExpiredAlert.tsx @@ -0,0 +1,30 @@ +import Alert from "@codegouvfr/react-dsfr/Alert"; +import { FC } from "react"; + +import SymfonyRouting from "../../modules/Routing"; +import { useAuthStore } from "../../stores/AuthStore"; + +const SessionExpiredAlert: FC = () => { + const { sessionExpired } = useAuthStore(); + + return ( + sessionExpired && ( + + Veuillez{" "} + + vous-reconnecter + {" "} + dans un nouvel onglet + + } + closable={true} + /> + ) + ); +}; + +export default SessionExpiredAlert; diff --git a/assets/contexts/UserContext.tsx b/assets/contexts/UserContext.tsx deleted file mode 100644 index 590c6f86..00000000 --- a/assets/contexts/UserContext.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Dispatch, FC, PropsWithChildren, createContext, useState } from "react"; - -import { CommunityMemberDto } from "../types/entrepot"; - -/** déclaration du type `User`, à tenir à jour avec le User de symfony */ -export type UserType = { - id: string; - email: string; - firstName: string; - lastName: string; - roles: string[]; - communitiesMember: CommunityMemberDto[]; - accountCreationDate: string; - lastApiCallDate: string; -}; - -/** déclaration du type de la valeur du contexte `UserContext` */ -export type UserContextType = { - user?: UserType; - - /** le setter d'un useState */ - setUser: Dispatch; -}; - -/** - * initialisation du contexte `UserContext` - * - * voir le hook useUser pour l'utilisation de ce contexte - */ -export const UserContext = createContext({ - user: undefined, - setUser: () => {}, -}); - -/** le Provider du contexte `UserContext` */ -export const UserContextProvider: FC = ({ children }) => { - const userFromTwig = (document.getElementById("user") as HTMLDivElement)?.dataset?.user ?? null; - const [user, setUser] = useState(userFromTwig === null ? null : JSON.parse(userFromTwig)); - - return {children}; -}; diff --git a/assets/hooks/useDatastoreList.tsx b/assets/hooks/useDatastoreList.tsx index 1fa00bcf..3a8f57eb 100644 --- a/assets/hooks/useDatastoreList.tsx +++ b/assets/hooks/useDatastoreList.tsx @@ -3,16 +3,16 @@ import { useQuery } from "@tanstack/react-query"; import api from "../api"; import RQKeys from "../modules/RQKeys"; import { CartesApiException } from "../modules/jsonFetch"; +import { useAuthStore } from "../stores/AuthStore"; import { Datastore } from "../types/app"; -import useUser from "./useUser"; export const useDatastoreList = () => { - const { user } = useUser(); + const { isAuthenticated } = useAuthStore(); return useQuery({ queryKey: RQKeys.datastore_list(), queryFn: ({ signal }) => api.user.getDatastoresList({ signal }), staleTime: 300000, - enabled: !!user, + enabled: isAuthenticated(), }); }; diff --git a/assets/hooks/useUser.tsx b/assets/hooks/useUser.tsx deleted file mode 100644 index 443802d8..00000000 --- a/assets/hooks/useUser.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from "react"; -import { UserContext, type UserContextType } from "../contexts/UserContext"; - -const useUser = (): UserContextType => { - const userContext = useContext(UserContext); - if (!userContext) { - throw new Error("useUserContext must be used within the UserContextProvider. UserContextProvider must wrap all the components that use useUserContext"); - } - return userContext; -}; -export default useUser; diff --git a/assets/modules/jsonFetch.ts b/assets/modules/jsonFetch.ts index 11e7e4cc..59613e79 100644 --- a/assets/modules/jsonFetch.ts +++ b/assets/modules/jsonFetch.ts @@ -1,3 +1,5 @@ +import { useAuthStore } from "../stores/AuthStore"; + /** doit avoir la même structure que l'erreur renvoyée par CartesApiExceptionSubscriber de Symfony */ export type CartesApiException = { code: number; @@ -46,8 +48,12 @@ export async function jsonFetch( const data = await response.json().catch(() => ({})); if (response.ok) { + useAuthStore.getState().setSessionExpired(false); resolve(data); } else { + if (hasSessionExpired(data)) { + useAuthStore.getState().setSessionExpired(true); + } reject(data); } } catch (error) { @@ -60,3 +66,7 @@ export async function jsonFetch( })(); }); } + +const hasSessionExpired = (error) => { + return error.code === 401 && error?.details?.controller === "App\\Controller\\Api\\ApiControllerInterface" && error?.details?.session_expired === true; +}; diff --git a/assets/pages/Home.tsx b/assets/pages/Home.tsx index 92907206..70d64b37 100644 --- a/assets/pages/Home.tsx +++ b/assets/pages/Home.tsx @@ -7,12 +7,37 @@ import { appRoot, useRoute } from "../router/router"; import hp from "../img/home/home.png"; import "../sass/pages/home.scss"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { useAlertsStore } from "../stores/AlertsStore"; const Home = () => { const { params } = useRoute(); + // const { pushAlert, removeAlert } = useAlerts(); + const alertsStore = useAlertsStore(); + return ( + + {params?.["authentication_failed"] === 1 && ( { - const { user } = useUser(); + const { user } = useAuthStore(); const route = useRoute(); const refMsg = useRef(null); diff --git a/assets/pages/contact/Contact.tsx b/assets/pages/contact/Contact.tsx index d60672f4..84af40b0 100644 --- a/assets/pages/contact/Contact.tsx +++ b/assets/pages/contact/Contact.tsx @@ -12,11 +12,11 @@ import { TranslationFunction } from "i18nifty/typeUtils/TranslationFunction"; import AppLayout from "../../components/Layout/AppLayout"; import Wait from "../../components/Utils/Wait"; -import useUser from "../../hooks/useUser"; import { declareComponentKeys, useTranslation, type Translations, ComponentKey } from "../../i18n/i18n"; import SymfonyRouting from "../../modules/Routing"; import { jsonFetch } from "../../modules/jsonFetch"; import { routes } from "../../router/router"; +import { useAuthStore } from "../../stores/AuthStore"; import { regex } from "../../utils"; import "../../sass/components/spinner.scss"; @@ -41,7 +41,7 @@ const schema = (t: TranslationFunction<"Contact", ComponentKey>) => const Contact = () => { const { t } = useTranslation({ Contact }); - const { user } = useUser(); + const { user } = useAuthStore(); const [isSending, setIsSending] = useState(false); const [error, setError] = useState(null); diff --git a/assets/pages/dashboard/DashboardPro.tsx b/assets/pages/dashboard/DashboardPro.tsx index 9e910a45..9d491d8d 100644 --- a/assets/pages/dashboard/DashboardPro.tsx +++ b/assets/pages/dashboard/DashboardPro.tsx @@ -5,14 +5,14 @@ import AppLayout from "../../components/Layout/AppLayout"; import LoadingText from "../../components/Utils/LoadingText"; import { datastoreNavItems } from "../../config/datastoreNavItems"; import { useDatastoreList } from "../../hooks/useDatastoreList"; -import useUser from "../../hooks/useUser"; import Translator from "../../modules/Translator"; import { routes } from "../../router/router"; +import { useAuthStore } from "../../stores/AuthStore"; const DashboardPro = () => { const datastoreListQuery = useDatastoreList(); const navItems = datastoreNavItems(); - const { user } = useUser(); + const { user } = useAuthStore(); return ( diff --git a/assets/pages/users/Me.tsx b/assets/pages/users/Me.tsx index c48b5695..9e7b5006 100644 --- a/assets/pages/users/Me.tsx +++ b/assets/pages/users/Me.tsx @@ -1,10 +1,10 @@ import AppLayout from "../../components/Layout/AppLayout"; import BtnBackToHome from "../../components/Utils/BtnBackToHome"; import functions from "../../functions"; -import useUser from "../../hooks/useUser"; +import { useAuthStore } from "../../stores/AuthStore"; const Me = () => { - const { user } = useUser(); + const { user } = useAuthStore(); return ( diff --git a/assets/router/RouterRenderer.tsx b/assets/router/RouterRenderer.tsx index 28ec0c18..7fd74ca9 100644 --- a/assets/router/RouterRenderer.tsx +++ b/assets/router/RouterRenderer.tsx @@ -2,11 +2,11 @@ import { JSX, Suspense, lazy } from "react"; import AppLayout from "../components/Layout/AppLayout"; import LoadingText from "../components/Utils/LoadingText"; -import useUser from "../hooks/useUser"; import SymfonyRouting from "../modules/Routing"; import Home from "../pages/Home"; import Redirect from "../pages/Redirect"; import PageNotFound from "../pages/error/PageNotFound"; +import { useAuthStore } from "../stores/AuthStore"; import { knownRoutes, publicRoutes, useRoute } from "./router"; const About = lazy(() => import("../pages/About")); @@ -46,7 +46,7 @@ const ServiceView = lazy(() => import("../pages/service/view/ServiceView")); function RouterRenderer() { const route = useRoute(); - const { user } = useUser(); + const { user } = useAuthStore(); // vérification si la route demandée est bien connue/enregistrée if (route.name === false || !knownRoutes.includes(route.name)) { diff --git a/assets/stores/AlertsStore.tsx b/assets/stores/AlertsStore.tsx new file mode 100644 index 00000000..9b7c697c --- /dev/null +++ b/assets/stores/AlertsStore.tsx @@ -0,0 +1,43 @@ +import { type AlertProps } from "@codegouvfr/react-dsfr/Alert"; +import { create } from "zustand"; + +interface AlertsStore { + /** Pile d'alertes */ + alerts: AlertProps[]; + + /** + * Ajoute une alerte + * + * ajoute nouvel objet Alert que s'il n'existe pas déjà dans la liste + */ + pushAlert: (alert: AlertProps) => void; + + /** Supprime une alerte par `AlertProps` ou l'id (string) */ + removeAlert: (alert: AlertProps | string) => void; +} + +const isEqual = (a1: AlertProps, a2: AlertProps) => { + return a1?.id === a2?.id; +}; + +export const useAlertsStore = create()((set) => ({ + alerts: [], + pushAlert: (alert) => + set((state) => { + const prevAlerts = state.alerts; + + const exists = prevAlerts.filter((pAlert) => isEqual(pAlert, alert)).length > 0; + const newAlerts = exists ? prevAlerts : [...prevAlerts, alert]; + + return { alerts: newAlerts }; + }), + removeAlert: (alert) => { + set((state) => { + const prevAlerts = state.alerts; + + const newAlerts = prevAlerts.filter((pAlert) => (typeof alert === "string" ? pAlert.id !== alert : !isEqual(pAlert, alert))); + + return { alerts: newAlerts }; + }); + }, +})); diff --git a/assets/stores/AuthStore.tsx b/assets/stores/AuthStore.tsx new file mode 100644 index 00000000..ece1252b --- /dev/null +++ b/assets/stores/AuthStore.tsx @@ -0,0 +1,22 @@ +import { create } from "zustand"; +import { User } from "../types/app"; + +interface AuthStore { + user: User | null; + setUser: (user: User | null) => void; + + isAuthenticated: () => boolean; + + sessionExpired: boolean; + setSessionExpired: (sessionExpired: boolean) => void; +} + +export const useAuthStore = create()((set, get) => ({ + user: null, + setUser: (user) => set(() => ({ user })), + + isAuthenticated: () => get().user !== null, + + sessionExpired: false, + setSessionExpired: (sessionExpired) => set(() => ({ sessionExpired })), +})); diff --git a/assets/types/app.ts b/assets/types/app.ts index 2fe63b21..34a22daf 100644 --- a/assets/types/app.ts +++ b/assets/types/app.ts @@ -22,8 +22,21 @@ import { ProcessingExecutionDetailResponseDto, CheckingExecutionDetailResponseDto, ProcessingExecutionOutputStoredDataDto, + CommunityMemberDto, } from "./entrepot"; +/** user */ +export type User = { + id: string; + email: string; + firstName: string; + lastName: string; + roles: string[]; + communitiesMember: CommunityMemberDto[]; + accountCreationDate: string; + lastApiCallDate: string; +}; + /** datastore */ export type Datastore = DatastoreDetailResponseDto; diff --git a/config/packages/web_profiler.yaml b/config/packages/web_profiler.yaml index b9461110..2d07f95a 100644 --- a/config/packages/web_profiler.yaml +++ b/config/packages/web_profiler.yaml @@ -10,8 +10,8 @@ when@dev: when@test: web_profiler: - toolbar: false + toolbar: true intercept_redirects: false framework: - profiler: { collect: false } + profiler: { enabled: true, collect: false } diff --git a/package.json b/package.json index 9b7bcdb6..866efb87 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "tsafe": "^1.6.5", "type-route": "^1.0.1", "uuid": "^9.0.0", - "yup": "^1.2.0" + "yup": "^1.2.0", + "zustand": "^4.4.4" }, "devDependencies": { "@babel/core": "^7.22.9", diff --git a/src/Constants/EntrepotApi/StoredDataTags.php b/src/Constants/EntrepotApi/StoredDataTags.php index df23b914..0bf5d556 100644 --- a/src/Constants/EntrepotApi/StoredDataTags.php +++ b/src/Constants/EntrepotApi/StoredDataTags.php @@ -5,4 +5,5 @@ final class StoredDataTags { public const DATASHEET_NAME = 'datasheet_name'; + public const STORED_DATA_ID = 'stored_data_id'; } diff --git a/src/Controller/Api/DatasheetController.php b/src/Controller/Api/DatasheetController.php index 63e3e6a9..a124c1eb 100644 --- a/src/Controller/Api/DatasheetController.php +++ b/src/Controller/Api/DatasheetController.php @@ -18,7 +18,7 @@ '/api/datastores/{datastoreId}/datasheet', name: 'cartesgouvfr_api_datasheet_', options: ['expose' => true], - condition: 'request.isXmlHttpRequest()' + // condition: 'request.isXmlHttpRequest()' )] class DatasheetController extends AbstractController implements ApiControllerInterface { @@ -72,6 +72,7 @@ public function getDetailed(string $datastoreId, string $datasheetName): JsonRes UploadTags::DATASHEET_NAME => $datasheetName, ], ]); + // dd($uploadList); $vectorDbList = $this->entrepotApiService->storedData->getAllDetailed($datastoreId, [ 'type' => StoredDataTypes::VECTOR_DB, diff --git a/src/Controller/Api/PyramidController.php b/src/Controller/Api/PyramidController.php index a407c08b..57b47cce 100644 --- a/src/Controller/Api/PyramidController.php +++ b/src/Controller/Api/PyramidController.php @@ -173,6 +173,7 @@ public function publish( $configuration = $this->entrepotApiService->configuration->add($datastoreId, $requestBody); $configuration = $this->entrepotApiService->configuration->addTags($datastoreId, $configuration['_id'], [ StoredDataTags::DATASHEET_NAME => $pyramid['tags'][StoredDataTags::DATASHEET_NAME], + StoredDataTags::STORED_DATA_ID => $pyramidId, ]); // Creation d'une offering diff --git a/src/Controller/Api/WfsController.php b/src/Controller/Api/WfsController.php index b48161c6..5b5b0f98 100644 --- a/src/Controller/Api/WfsController.php +++ b/src/Controller/Api/WfsController.php @@ -93,6 +93,7 @@ public function add( $configuration = $this->entrepotApiService->configuration->add($datastoreId, $body); $configuration = $this->entrepotApiService->configuration->addTags($datastoreId, $configuration['_id'], [ StoredDataTags::DATASHEET_NAME => $storedData['tags'][StoredDataTags::DATASHEET_NAME], + StoredDataTags::STORED_DATA_ID => $storedDataId, ]); // Creation d'une offering diff --git a/src/Listener/InternalApiSubscriber.php b/src/Listener/InternalApiSubscriber.php index 2918a9a9..ae82cf6e 100644 --- a/src/Listener/InternalApiSubscriber.php +++ b/src/Listener/InternalApiSubscriber.php @@ -5,7 +5,9 @@ use App\Controller\Api\ApiControllerInterface; use App\Exception\CartesApiException; use App\Security\KeycloakToken; +use App\Security\User; use League\OAuth2\Client\Token\AccessToken; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Response; @@ -19,7 +21,8 @@ class InternalApiSubscriber implements EventSubscriberInterface { public function __construct( - private ParameterBagInterface $parameters + private ParameterBagInterface $parameters, + private Security $security ) { } @@ -52,12 +55,16 @@ public function onKernelController(ControllerEvent $event): void /** @var AccessToken */ $accessToken = $session->get(KeycloakToken::SESSION_KEY); - // TODO préciser que c'est l'API interne - if (null == $accessToken + $user = $this->security->getUser(); + + if (null === $user + || !($user instanceof User) + || null == $accessToken || (null != $accessToken && $accessToken->hasExpired())) { - throw new CartesApiException(Response::$statusTexts[Response::HTTP_UNAUTHORIZED], Response::HTTP_UNAUTHORIZED, ['controller' => ApiControllerInterface::class]); + throw new CartesApiException(Response::$statusTexts[Response::HTTP_UNAUTHORIZED], Response::HTTP_UNAUTHORIZED, ['controller' => ApiControllerInterface::class, 'session_expired' => true]); } } + // throw new CartesApiException(Response::$statusTexts[Response::HTTP_UNAUTHORIZED], Response::HTTP_UNAUTHORIZED, ['controller' => ApiControllerInterface::class, 'session_expired' => true]); } } } diff --git a/yarn.lock b/yarn.lock index 9b7a226b..0ac97e54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8487,6 +8487,11 @@ use-deep-compare-effect@^1.8.1: "@babel/runtime" "^7.12.5" dequal "^2.0.2" +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -8884,6 +8889,13 @@ yup@^1.2.0: toposort "^2.0.2" type-fest "^2.19.0" +zustand@^4.4.4: + version "4.4.4" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.4.tgz#cc06202219972bd61cef1fd10105e6384ae1d5cf" + integrity sha512-5UTUIAiHMNf5+mFp7/AnzJXS7+XxktULFN0+D1sCiZWyX7ZG+AQpqs2qpYrynRij4QvoDdCD+U+bmg/cG3Ucxw== + dependencies: + use-sync-external-store "1.2.0" + zwitch@^2.0.0, zwitch@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"