From e8db0176a17d11d5608c4f9d14b5dd531c7dd651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Tue, 15 Apr 2025 13:32:12 +0200 Subject: [PATCH] Remove recoil sync (#11569) Recoil-sync was causing issues with Firefox, replacing it with a simpler mechanism to hydrate variables on page load --------- Co-authored-by: etiennejouan --- packages/twenty-front/package.json | 1 - .../src/modules/app/components/App.tsx | 41 ++++++------ .../effect-components/PageChangeEffect.tsx | 7 +- .../app/hooks/useInitializeQueryParamState.ts | 66 +++++++++++++++++++ .../states/isQueryParamInitializedState.ts | 6 ++ .../src/modules/auth/components/AuthModal.tsx | 12 +--- .../auth/components/VerifyEmailEffect.tsx | 4 -- .../src/modules/auth/hooks/useAuth.ts | 4 -- .../modules/auth/states/animateModalState.ts | 26 -------- .../states/billingCheckoutSessionState.ts | 26 -------- ...useBuildSearchParamsFromUrlSyncedStates.ts | 3 - .../hooks/useImpersonationRedirect.ts | 4 -- .../ui/layout/modal/components/Modal.tsx | 4 +- .../layout/page/components/DefaultLayout.tsx | 5 +- ...ultiWorkspaceDropdownDefaultComponents.tsx | 3 - .../src/pages/onboarding/ChooseYourPlan.tsx | 2 +- yarn.lock | 27 -------- 17 files changed, 102 insertions(+), 139 deletions(-) create mode 100644 packages/twenty-front/src/modules/app/hooks/useInitializeQueryParamState.ts create mode 100644 packages/twenty-front/src/modules/app/states/isQueryParamInitializedState.ts delete mode 100644 packages/twenty-front/src/modules/auth/states/animateModalState.ts diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index d757119da..238b85cae 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -54,7 +54,6 @@ "buffer": "^6.0.3", "docx": "^9.1.0", "file-saver": "^2.0.5", - "recoil-sync": "^0.2.0", "transliteration": "^2.3.5", "twenty-shared": "workspace:*", "twenty-ui": "workspace:*" diff --git a/packages/twenty-front/src/modules/app/components/App.tsx b/packages/twenty-front/src/modules/app/components/App.tsx index 6ee92bd1f..fbc5ed335 100644 --- a/packages/twenty-front/src/modules/app/components/App.tsx +++ b/packages/twenty-front/src/modules/app/components/App.tsx @@ -9,35 +9,32 @@ import { i18n } from '@lingui/core'; import { I18nProvider } from '@lingui/react'; import { HelmetProvider } from 'react-helmet-async'; import { RecoilRoot } from 'recoil'; -import { RecoilURLSyncJSON } from 'recoil-sync'; -import { initialI18nActivate } from '~/utils/i18n/initialI18nActivate'; import { IconsProvider } from 'twenty-ui/display'; +import { initialI18nActivate } from '~/utils/i18n/initialI18nActivate'; initialI18nActivate(); export const App = () => { return ( - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx index 9cad8927d..01ccee726 100644 --- a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx @@ -26,6 +26,7 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope import { isDefined } from 'twenty-shared/utils'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation'; +import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState'; // TODO: break down into smaller functions and / or hooks // - moved usePageChangeEffectNavigateLocation into dedicated hook @@ -44,6 +45,8 @@ export const PageChangeEffect = () => { const eventTracker = useEventTracker(); + const { initializeQueryParamState } = useInitializeQueryParamState(); + //TODO: refactor useResetTableRowSelection hook to not throw when the argument `recordTableId` is an empty string // - replace CoreObjectNamePlural.Person const objectNamePlural = @@ -64,10 +67,12 @@ export const PageChangeEffect = () => { }, [location, previousLocation, executeTasksOnAnyLocationChange]); useEffect(() => { + initializeQueryParamState(); + if (isDefined(pageChangeEffectNavigateLocation)) { navigate(pageChangeEffectNavigateLocation); } - }, [navigate, pageChangeEffectNavigateLocation]); + }, [navigate, pageChangeEffectNavigateLocation, initializeQueryParamState]); useEffect(() => { const isLeavingRecordIndexPage = !!matchPath( diff --git a/packages/twenty-front/src/modules/app/hooks/useInitializeQueryParamState.ts b/packages/twenty-front/src/modules/app/hooks/useInitializeQueryParamState.ts new file mode 100644 index 000000000..d443ae906 --- /dev/null +++ b/packages/twenty-front/src/modules/app/hooks/useInitializeQueryParamState.ts @@ -0,0 +1,66 @@ +import { useRecoilCallback } from 'recoil'; + +import { isQueryParamInitializedState } from '@/app/states/isQueryParamInitializedState'; +import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState'; +import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type'; +import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue'; + +// Initialize state that are hydrated from query parameters +// We used to use recoil-sync to do this, but it was causing issues with Firefox +export const useInitializeQueryParamState = () => { + const initializeQueryParamState = useRecoilCallback( + ({ set, snapshot }) => + () => { + const isInitialized = snapshot + .getLoadable(isQueryParamInitializedState) + .getValue(); + + if (!isInitialized) { + const handlers = { + billingCheckoutSession: (value: string) => { + try { + const parsedValue = JSON.parse(decodeURIComponent(value)); + + if ( + typeof parsedValue === 'object' && + parsedValue !== null && + 'plan' in parsedValue && + 'interval' in parsedValue && + 'requirePaymentMethod' in parsedValue + ) { + set( + billingCheckoutSessionState, + parsedValue as BillingCheckoutSession, + ); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error( + 'Failed to parse billingCheckoutSession from URL', + error, + ); + set( + billingCheckoutSessionState, + BILLING_CHECKOUT_SESSION_DEFAULT_VALUE, + ); + } + }, + }; + + const queryParams = new URLSearchParams(window.location.search); + + for (const [paramName, handler] of Object.entries(handlers)) { + const value = queryParams.get(paramName); + if (value !== null) { + handler(value); + } + } + + set(isQueryParamInitializedState, true); + } + }, + [], + ); + + return { initializeQueryParamState }; +}; diff --git a/packages/twenty-front/src/modules/app/states/isQueryParamInitializedState.ts b/packages/twenty-front/src/modules/app/states/isQueryParamInitializedState.ts new file mode 100644 index 000000000..cd4d50739 --- /dev/null +++ b/packages/twenty-front/src/modules/app/states/isQueryParamInitializedState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui/utilities'; + +export const isQueryParamInitializedState = createState({ + key: 'isQueryParamInitializedState', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/auth/components/AuthModal.tsx b/packages/twenty-front/src/modules/auth/components/AuthModal.tsx index 8277ee828..b461837e0 100644 --- a/packages/twenty-front/src/modules/auth/components/AuthModal.tsx +++ b/packages/twenty-front/src/modules/auth/components/AuthModal.tsx @@ -10,18 +10,10 @@ const StyledContent = styled.div` type AuthModalProps = { children: React.ReactNode; - isOpenAnimated?: boolean; }; -export const AuthModal = ({ - children, - isOpenAnimated = true, -}: AuthModalProps) => ( - +export const AuthModal = ({ children }: AuthModalProps) => ( + {children} diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx index c0cb3d4a3..9c104d7a3 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx @@ -4,12 +4,10 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin'; -import { animateModalState } from '@/auth/states/animateModalState'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; import { useLingui } from '@lingui/react/macro'; import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; import { useNavigateApp } from '~/hooks/useNavigateApp'; import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; import { EmailVerificationSent } from '../sign-in-up/components/EmailVerificationSent'; @@ -21,7 +19,6 @@ export const VerifyEmailEffect = () => { const [searchParams] = useSearchParams(); const [isError, setIsError] = useState(false); - const setAnimateModal = useSetRecoilState(animateModalState); const email = searchParams.get('email'); const emailVerificationToken = searchParams.get('emailVerificationToken'); @@ -52,7 +49,6 @@ export const VerifyEmailEffect = () => { const workspaceUrl = getWorkspaceUrl(workspaceUrls); if (workspaceUrl.slice(0, -1) !== window.location.origin) { - setAnimateModal(false); return await redirectToWorkspaceDomain(workspaceUrl, AppPath.Verify, { loginToken: loginToken.token, }); diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 4dae0dafb..438e0aa92 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -41,7 +41,6 @@ import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTi import { currentUserState } from '../states/currentUserState'; import { tokenPairState } from '../states/tokenPairState'; -import { animateModalState } from '@/auth/states/animateModalState'; import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; import { SignInUpStep, @@ -115,7 +114,6 @@ export const useAuth = () => { const goToRecoilSnapshot = useGotoRecoilSnapshot(); const setDateTimeFormat = useSetRecoilState(dateTimeFormatState); - const setAnimateModal = useSetRecoilState(animateModalState); const [, setSearchParams] = useSearchParams(); @@ -422,7 +420,6 @@ export const useAuth = () => { } if (isMultiWorkspaceEnabled) { - setAnimateModal(false); return await redirectToWorkspaceDomain( getWorkspaceUrl(signUpResult.data.signUp.workspace.workspaceUrls), isEmailVerificationRequired ? AppPath.SignInUp : AppPath.Verify, @@ -446,7 +443,6 @@ export const useAuth = () => { handleGetAuthTokensFromLoginToken, setSignInUpStep, setSearchParams, - setAnimateModal, isEmailVerificationRequired, redirectToWorkspaceDomain, ], diff --git a/packages/twenty-front/src/modules/auth/states/animateModalState.ts b/packages/twenty-front/src/modules/auth/states/animateModalState.ts deleted file mode 100644 index 6c2cd96da..000000000 --- a/packages/twenty-front/src/modules/auth/states/animateModalState.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { urlSyncEffect } from 'recoil-sync'; -import { createState } from 'twenty-ui/utilities'; - -export const animateModalState = createState({ - key: 'animateModalState', - defaultValue: true, - effects: [ - urlSyncEffect({ - itemKey: 'animateModal', - refine: (value: unknown) => { - if (typeof value === 'boolean') { - return { - type: 'success', - value: value as boolean, - warnings: [], - } as const; - } - return { - type: 'failure', - message: 'Invalid animateModalState', - path: [] as any, - } as const; - }, - }), - ], -}); diff --git a/packages/twenty-front/src/modules/auth/states/billingCheckoutSessionState.ts b/packages/twenty-front/src/modules/auth/states/billingCheckoutSessionState.ts index dcb24afeb..b3bd10538 100644 --- a/packages/twenty-front/src/modules/auth/states/billingCheckoutSessionState.ts +++ b/packages/twenty-front/src/modules/auth/states/billingCheckoutSessionState.ts @@ -1,34 +1,8 @@ import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type'; import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue'; -import { syncEffect } from 'recoil-sync'; import { createState } from 'twenty-ui/utilities'; export const billingCheckoutSessionState = createState({ key: 'billingCheckoutSessionState', defaultValue: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE, - effects: [ - syncEffect({ - itemKey: 'billingCheckoutSession', - refine: (value: unknown) => { - if ( - typeof value === 'object' && - value !== null && - 'plan' in value && - 'interval' in value && - 'requirePaymentMethod' in value - ) { - return { - type: 'success', - value: value as BillingCheckoutSession, - warnings: [], - } as const; - } - return { - type: 'failure', - message: 'Invalid BillingCheckoutSessionState', - path: [] as any, - } as const; - }, - }), - ], }); diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates.ts index 9996de191..b4fea3aa1 100644 --- a/packages/twenty-front/src/modules/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates.ts +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates.ts @@ -1,4 +1,3 @@ -import { animateModalState } from '@/auth/states/animateModalState'; import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState'; import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue'; import { useRecoilCallback } from 'recoil'; @@ -7,7 +6,6 @@ export const useBuildSearchParamsFromUrlSyncedStates = () => { const buildSearchParamsFromUrlSyncedStates = useRecoilCallback( ({ snapshot }) => async () => { - const animateModal = snapshot.getLoadable(animateModalState).getValue(); const billingCheckoutSession = snapshot .getLoadable(billingCheckoutSessionState) .getValue(); @@ -18,7 +16,6 @@ export const useBuildSearchParamsFromUrlSyncedStates = () => { billingCheckoutSession: JSON.stringify(billingCheckoutSession), } : {}), - ...(animateModal === false ? { animateModal: 'false' } : {}), }; return output; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonationRedirect.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonationRedirect.ts index 8e4e4abae..c6210ab8e 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonationRedirect.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonationRedirect.ts @@ -1,19 +1,15 @@ -import { animateModalState } from '@/auth/states/animateModalState'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; import { AppPath } from '@/types/AppPath'; -import { useSetRecoilState } from 'recoil'; import { WorkspaceUrls } from '~/generated/graphql'; import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; export const useImpersonationRedirect = () => { const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); - const setAnimateModal = useSetRecoilState(animateModalState); const executeImpersonationRedirect = async ( workspaceUrls: WorkspaceUrls, loginToken: string, ) => { - setAnimateModal(false); return await redirectToWorkspaceDomain( getWorkspaceUrl(workspaceUrls), AppPath.Verify, diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx index 150c1e9a9..cad149e21 100644 --- a/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx +++ b/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx @@ -174,7 +174,6 @@ export type ModalProps = React.PropsWithChildren & { className?: string; hotkeyScope?: ModalHotkeyScope; onEnter?: () => void; - isOpenAnimated?: boolean; modalVariant?: ModalVariants; } & ( | { isClosable: true; onClose: () => void } @@ -197,7 +196,6 @@ export const Modal = ({ isClosable = false, onClose, modalVariant = 'primary', - isOpenAnimated = true, }: ModalProps) => { const isMobile = useIsMobile(); const modalRef = useRef(null); @@ -263,7 +261,7 @@ export const Modal = ({ ref={modalRef} size={size} padding={padding} - initial={isOpenAnimated ? 'hidden' : 'visible'} + initial="hidden" animate="visible" exit="exit" layout diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx index 72b57946f..f8a436a53 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx @@ -1,5 +1,4 @@ import { AuthModal } from '@/auth/components/AuthModal'; -import { animateModalState } from '@/auth/states/animateModalState'; import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter'; import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback'; @@ -19,7 +18,6 @@ import { Global, css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { AnimatePresence, LayoutGroup, motion } from 'framer-motion'; import { Outlet } from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; import { useScreenSize } from 'twenty-ui/utilities'; const StyledLayout = styled.div` @@ -65,7 +63,6 @@ export const DefaultLayout = () => { const windowsWidth = useScreenSize().width; const showAuthModal = useShowAuthModal(); const useShowFullScreen = useShowFullscreen(); - const animateModal = useRecoilValue(animateModalState); return ( <> @@ -111,7 +108,7 @@ export const DefaultLayout = () => { - + diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx index da422e79d..bebc4de78 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx @@ -1,7 +1,6 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo'; import { useAuth } from '@/auth/hooks/useAuth'; -import { animateModalState } from '@/auth/states/animateModalState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { Workspaces, workspacesState } from '@/auth/states/workspaces'; import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl'; @@ -67,7 +66,6 @@ export const MultiWorkspaceDropdownDefaultComponents = () => { const setMultiWorkspaceDropdownState = useSetRecoilState( multiWorkspaceDropdownState, ); - const setAnimateModal = useSetRecoilState(animateModalState); const handleChange = async (workspace: Workspaces[0]) => { redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls)); @@ -76,7 +74,6 @@ export const MultiWorkspaceDropdownDefaultComponents = () => { const createWorkspace = () => { signUpInNewWorkspaceMutation({ onCompleted: async (data) => { - setAnimateModal(false); return await redirectToWorkspaceDomain( getWorkspaceUrl(data.signUpInNewWorkspace.workspace.workspaceUrls), AppPath.Verify, diff --git a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx index 890c0704b..2bdeb3ce8 100644 --- a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx +++ b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx @@ -172,7 +172,7 @@ export const ChooseYourPlan = () => { ?.baseProduct.name; return ( - + {isDefined(baseProductPrice) && isDefined(billing) ? ( <> diff --git a/yarn.lock b/yarn.lock index 8b1dfc268..a0a0282d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16495,13 +16495,6 @@ __metadata: languageName: node linkType: hard -"@recoiljs/refine@npm:^0.1.1": - version: 0.1.1 - resolution: "@recoiljs/refine@npm:0.1.1" - checksum: 10c0/27ca1c4ea500b1b99a3af5ee48d6749310e5138e83b87ddfb41304e2222fa64567acb985f340334ab73980202ab277a0f133c40817fbec786076c06bfb3f5363 - languageName: node - linkType: hard - "@redis/bloom@npm:1.2.0, @redis/bloom@npm:^1.2.0": version: 1.2.0 resolution: "@redis/bloom@npm:1.2.0" @@ -50073,18 +50066,6 @@ __metadata: languageName: node linkType: hard -"recoil-sync@npm:^0.2.0": - version: 0.2.0 - resolution: "recoil-sync@npm:0.2.0" - dependencies: - "@recoiljs/refine": "npm:^0.1.1" - transit-js: "npm:^0.8.874" - peerDependencies: - recoil: ">=0.7.3" - checksum: 10c0/f3a671a3cfcecadb5bbb22d47b3b040721be1013ee91d4fa0570a64ca1707eb68e818b291a82e133551adf4a2dd2fc9a0715d550c1f1e3db82f3b8d9169e2a5a - languageName: node - linkType: hard - "recoil@npm:^0.7.7": version: 0.7.7 resolution: "recoil@npm:0.7.7" @@ -54388,13 +54369,6 @@ __metadata: languageName: node linkType: hard -"transit-js@npm:^0.8.874": - version: 0.8.874 - resolution: "transit-js@npm:0.8.874" - checksum: 10c0/6ca0b413f1e3780a4a56b9bbde54b67a2ffefda1c052f8dc68bf28c1a3df4c29baa6f17f60484c9f2fb056a8c7dcd1fe04d24e091fb99afe1525e4d4406ad58a - languageName: node - linkType: hard - "transliteration@npm:^2.3.5": version: 2.3.5 resolution: "transliteration@npm:2.3.5" @@ -55005,7 +54979,6 @@ __metadata: eslint-plugin-unused-imports: "npm:^3.0.0" file-saver: "npm:^2.0.5" optionator: "npm:^0.9.1" - recoil-sync: "npm:^0.2.0" transliteration: "npm:^2.3.5" twenty-shared: "workspace:*" twenty-ui: "workspace:*"