diff --git a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx index 1fbb47380d..1d1e3f73dd 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx @@ -6,6 +6,7 @@ import { IsMinimalMetadataReadyEffect } from '@/metadata-store/effect-components import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider'; import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect'; +import { SignOutOnOtherTabSignOutEffect } from '@/auth/effect-components/SignOutOnOtherTabSignOutEffect'; import { AuthProvider } from '@/auth/components/AuthProvider'; import { CaptchaProvider } from '@/captcha/components/CaptchaProvider'; import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; @@ -80,6 +81,7 @@ export const AppRouterProviders = () => { + diff --git a/packages/twenty-front/src/modules/auth/effect-components/SignOutOnOtherTabSignOutEffect.tsx b/packages/twenty-front/src/modules/auth/effect-components/SignOutOnOtherTabSignOutEffect.tsx new file mode 100644 index 0000000000..4dbe8721cb --- /dev/null +++ b/packages/twenty-front/src/modules/auth/effect-components/SignOutOnOtherTabSignOutEffect.tsx @@ -0,0 +1,17 @@ +import { useAuth } from '@/auth/hooks/useAuth'; +import { subscribeToSignOutFromOtherTabs } from '@/auth/utils/crossTabSignOut'; +import { useEffect } from 'react'; + +export const SignOutOnOtherTabSignOutEffect = () => { + const { clearSession } = useAuth(); + + useEffect(() => { + const unsubscribe = subscribeToSignOutFromOtherTabs(() => { + clearSession(); + }); + + return unsubscribe; + }, [clearSession]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 63772012cb..51081a0cf9 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -23,6 +23,8 @@ import { } from '~/generated-metadata/graphql'; import { tokenPairState } from '@/auth/states/tokenPairState'; +import { clearSessionLocalStorageKeys } from '@/auth/utils/clearSessionLocalStorageKeys'; +import { broadcastSignOutToOtherTabs } from '@/auth/utils/crossTabSignOut'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; @@ -34,7 +36,8 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace'; -import { useLoadMockedMinimalMetadata } from '@/metadata-store/hooks/useLoadMockedMinimalMetadata'; +import { useLoadMockedMetadata } from '@/metadata-store/hooks/useLoadMockedMetadata'; +import { preloadMockedMetadata } from '@/metadata-store/utils/preloadMockedMetadata'; import { lastAuthenticatedMethodState } from '@/auth/states/lastAuthenticatedMethodState'; import { loginTokenState } from '@/auth/states/loginTokenState'; import { @@ -86,7 +89,7 @@ export const useAuth = () => { const { loadCurrentUser } = useLoadCurrentUser(); const { clearSseClient } = useClearSseClient(); - const { loadMockedMinimalMetadata } = useLoadMockedMinimalMetadata(); + const { applyMockedMetadata } = useLoadMockedMetadata(); const { createWorkspace } = useSignUpInNewWorkspace(); const setSignInUpStep = useSetAtomState(signInUpStepState); @@ -126,6 +129,9 @@ export const useAuth = () => { const clearSession = useCallback(async () => { clearSseClient(); + store.set(isAppEffectRedirectEnabledState.atom, false); + + const mockedData = await preloadMockedMetadata(); const authProvidersValue = store.get(workspaceAuthProvidersState.atom); const domainConfigurationValue = store.get(domainConfigurationState.atom); @@ -137,10 +143,8 @@ export const useAuth = () => { isCaptchaScriptLoadedState.atom, ); - store.set(isAppEffectRedirectEnabledState.atom, false); - sessionStorage.clear(); - localStorage.clear(); + clearSessionLocalStorageKeys(); store.set(workspaceAuthProvidersState.atom, authProvidersValue); store.set(workspacePublicDataState.atom, workspacePublicDataValue); @@ -161,16 +165,17 @@ export const useAuth = () => { store.set(loginTokenState.atom, null); store.set(signInUpStepState.atom, SignInUpStep.Init); + applyMockedMetadata(mockedData); + await client.clearStore(); setLastAuthenticateWorkspaceDomain(null); - await loadMockedMinimalMetadata(); navigate(AppPath.SignInUp); store.set(isAppEffectRedirectEnabledState.atom, true); }, [ clearSseClient, client, setLastAuthenticateWorkspaceDomain, - loadMockedMinimalMetadata, + applyMockedMetadata, navigate, store, ]); @@ -476,6 +481,7 @@ export const useAuth = () => { ); const handleSignOut = useCallback(async () => { + broadcastSignOutToOtherTabs(); await clearSession(); if (isCaptchaScriptLoaded) await requestFreshCaptchaToken(); }, [clearSession, isCaptchaScriptLoaded, requestFreshCaptchaToken]); diff --git a/packages/twenty-front/src/modules/auth/utils/clearAllSessionLocalStorageKeys.ts b/packages/twenty-front/src/modules/auth/utils/clearAllSessionLocalStorageKeys.ts new file mode 100644 index 0000000000..a409f16f9c --- /dev/null +++ b/packages/twenty-front/src/modules/auth/utils/clearAllSessionLocalStorageKeys.ts @@ -0,0 +1,18 @@ +import { safeRemoveLocalStorageItems } from '@/auth/utils/safeRemoveLocalStorageItems'; +import { + ALL_METADATA_ENTITY_KEYS, + type MetadataEntityKey, +} from '@/metadata-store/states/metadataStoreState'; +import { clearSessionLocalStorageKeys } from './clearSessionLocalStorageKeys'; + +const METADATA_STORE_PREFIX = 'metadataStoreState__'; + +const getMetadataStoreKeys = (): string[] => + ALL_METADATA_ENTITY_KEYS.map( + (key: MetadataEntityKey) => `${METADATA_STORE_PREFIX}${key}`, + ); + +export const clearAllSessionLocalStorageKeys = () => { + clearSessionLocalStorageKeys(); + safeRemoveLocalStorageItems(getMetadataStoreKeys()); +}; diff --git a/packages/twenty-front/src/modules/auth/utils/clearSessionLocalStorageKeys.ts b/packages/twenty-front/src/modules/auth/utils/clearSessionLocalStorageKeys.ts new file mode 100644 index 0000000000..60c1bf8b50 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/utils/clearSessionLocalStorageKeys.ts @@ -0,0 +1,13 @@ +import { safeRemoveLocalStorageItems } from '@/auth/utils/safeRemoveLocalStorageItems'; + +const SESSION_KEYS_TO_CLEAR = [ + 'lastVisitedObjectMetadataItemIdState', + 'lastVisitedViewPerObjectMetadataItemState', + 'playgroundApiKeyState', + 'ai/agentChatDraftsByThreadIdState', + 'locale', +]; + +export const clearSessionLocalStorageKeys = () => { + safeRemoveLocalStorageItems(SESSION_KEYS_TO_CLEAR); +}; diff --git a/packages/twenty-front/src/modules/auth/utils/crossTabSignOut.ts b/packages/twenty-front/src/modules/auth/utils/crossTabSignOut.ts new file mode 100644 index 0000000000..6274e83fc4 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/utils/crossTabSignOut.ts @@ -0,0 +1,41 @@ +const SIGN_OUT_CHANNEL_NAME = 'twenty-sign-out'; + +let sharedChannel: BroadcastChannel | null = null; + +const getSharedSignOutChannel = (): BroadcastChannel | null => { + if (sharedChannel) { + return sharedChannel; + } + + try { + sharedChannel = new BroadcastChannel(SIGN_OUT_CHANNEL_NAME); + } catch { + return null; + } + + return sharedChannel; +}; + +export const broadcastSignOutToOtherTabs = () => { + getSharedSignOutChannel()?.postMessage({ type: 'sign-out' }); +}; + +export const subscribeToSignOutFromOtherTabs = ( + callback: () => void, +): (() => void) => { + const channel = getSharedSignOutChannel(); + + if (!channel) { + return () => {}; + } + + channel.onmessage = (event: MessageEvent) => { + if (event.data?.type === 'sign-out') { + callback(); + } + }; + + return () => { + channel.onmessage = null; + }; +}; diff --git a/packages/twenty-front/src/modules/auth/utils/safeRemoveLocalStorageItems.ts b/packages/twenty-front/src/modules/auth/utils/safeRemoveLocalStorageItems.ts new file mode 100644 index 0000000000..c1863f30fa --- /dev/null +++ b/packages/twenty-front/src/modules/auth/utils/safeRemoveLocalStorageItems.ts @@ -0,0 +1,9 @@ +export const safeRemoveLocalStorageItems = (keys: string[]) => { + for (const key of keys) { + try { + localStorage.removeItem(key); + } catch { + // noop + } + } +}; diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/MetadataStoreSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/MetadataStoreSSEEffect.tsx index 5cdf513fc5..db665dca5d 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/MetadataStoreSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/MetadataStoreSSEEffect.tsx @@ -1,5 +1,5 @@ import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { type MetadataEntityKey } from '@/metadata-store/states/metadataStoreState'; import { type MetadataEntityTypeMap } from '@/metadata-store/types/MetadataEntityTypeMap'; import { mapAllMetadataNameToEntityKey } from '@/metadata-store/utils/mapAllMetadataNameToEntityKey'; @@ -8,7 +8,8 @@ import { isDefined } from 'twenty-shared/utils'; type AnyMetadataEntity = MetadataEntityTypeMap[MetadataEntityKey]; export const MetadataStoreSSEEffect = () => { - const { addToDraft, removeFromDraft, applyChanges } = useMetadataStore(); + const { addToDraft, removeFromDraft, applyChanges } = + useUpdateMetadataStoreDraft(); useListenToMetadataOperationBrowserEvent({ onMetadataOperationBrowserEvent: (eventDetail) => { diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/MinimalMetadataLoadEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/MinimalMetadataLoadEffect.tsx index 10bfb7c9a5..3bb238ddb1 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/MinimalMetadataLoadEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/MinimalMetadataLoadEffect.tsx @@ -2,8 +2,9 @@ import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState'; import { useLoadMinimalMetadata } from '@/metadata-store/hooks/useLoadMinimalMetadata'; -import { useLoadMockedMinimalMetadata } from '@/metadata-store/hooks/useLoadMockedMinimalMetadata'; +import { useLoadMockedMetadata } from '@/metadata-store/hooks/useLoadMockedMetadata'; import { useLoadStaleMetadataEntities } from '@/metadata-store/hooks/useLoadStaleMetadataEntities'; +import { metadataLoadedVersionState } from '@/metadata-store/states/metadataLoadedVersionState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useEffect, useState } from 'react'; import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace'; @@ -25,10 +26,14 @@ export const MinimalMetadataLoadEffect = () => { const hasAccessTokenPair = useHasAccessTokenPair(); const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState); const currentWorkspace = useAtomStateValue(currentWorkspaceState); - const [loadedState, setLoadedState] = useState('none'); + const metadataLoadedVersion = useAtomStateValue(metadataLoadedVersionState); + const [lastMetadataLoadData, setLastMetadataLoadData] = useState<{ + state: LoadedState; + version: number; + }>({ state: 'none', version: -1 }); const { loadMinimalMetadata } = useLoadMinimalMetadata(); - const { loadMockedMinimalMetadata } = useLoadMockedMinimalMetadata(); + const { loadMockedMetadataAtomic } = useLoadMockedMetadata(); const { loadStaleMetadataEntities } = useLoadStaleMetadataEntities(); const isActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace); @@ -39,15 +44,25 @@ export const MinimalMetadataLoadEffect = () => { ); useEffect(() => { - if (!isCurrentUserLoaded || loadedState === desiredLoadState) { + if (!isCurrentUserLoaded) { return; } - setLoadedState(desiredLoadState); + const versionChanged = + metadataLoadedVersion !== lastMetadataLoadData.version; + + if (!versionChanged && lastMetadataLoadData.state === desiredLoadState) { + return; + } + + setLastMetadataLoadData({ + state: desiredLoadState, + version: metadataLoadedVersion, + }); const performLoad = async () => { if (desiredLoadState === 'mocked') { - await loadMockedMinimalMetadata(); + await loadMockedMetadataAtomic(); return; } @@ -64,9 +79,10 @@ export const MinimalMetadataLoadEffect = () => { hasAccessTokenPair, isActiveWorkspace, desiredLoadState, - loadedState, + lastMetadataLoadData, + metadataLoadedVersion, loadMinimalMetadata, - loadMockedMinimalMetadata, + loadMockedMetadataAtomic, loadStaleMetadataEntities, ]); diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useInvalidateMetadataStore.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useInvalidateMetadataStore.ts new file mode 100644 index 0000000000..e2375dafc8 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/hooks/useInvalidateMetadataStore.ts @@ -0,0 +1,23 @@ +import { metadataLoadedVersionState } from '@/metadata-store/states/metadataLoadedVersionState'; +import { + ALL_METADATA_ENTITY_KEYS, + metadataStoreState, +} from '@/metadata-store/states/metadataStoreState'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; + +export const useInvalidateMetadataStore = () => { + const store = useStore(); + + const invalidateMetadataStore = useCallback(() => { + for (const key of ALL_METADATA_ENTITY_KEYS) { + store.set(metadataStoreState.atomFamily(key), (prev) => ({ + ...prev, + currentCollectionHash: undefined, + })); + } + store.set(metadataLoadedVersionState.atom, (prev) => prev + 1); + }, [store]); + + return { invalidateMetadataStore }; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMockedMetadata.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMockedMetadata.ts new file mode 100644 index 0000000000..e96fc687cb --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMockedMetadata.ts @@ -0,0 +1,83 @@ +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; +import { metadataStoreState } from '@/metadata-store/states/metadataStoreState'; +import { + preloadMockedMetadata, + type PreloadedMockedMetadata, +} from '@/metadata-store/utils/preloadMockedMetadata'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; + +const MOCKED_COLLECTION_HASH = 'mocked'; + +export const useLoadMockedMetadata = () => { + const store = useStore(); + const { replaceDraft, applyChanges, resetMetadataStore } = + useUpdateMetadataStoreDraft(); + + const applyMockedMetadata = useCallback( + (data: PreloadedMockedMetadata) => { + resetMetadataStore(); + + replaceDraft( + 'objectMetadataItems', + data.flatObjects, + MOCKED_COLLECTION_HASH, + ); + replaceDraft( + 'fieldMetadataItems', + data.flatFields, + MOCKED_COLLECTION_HASH, + ); + replaceDraft( + 'indexMetadataItems', + data.flatIndexes, + MOCKED_COLLECTION_HASH, + ); + replaceDraft('views', data.flatViews, MOCKED_COLLECTION_HASH); + replaceDraft('viewFields', data.flatViewFields, MOCKED_COLLECTION_HASH); + replaceDraft('viewFilters', data.flatViewFilters, MOCKED_COLLECTION_HASH); + replaceDraft('viewSorts', data.flatViewSorts, MOCKED_COLLECTION_HASH); + replaceDraft('viewGroups', data.flatViewGroups, MOCKED_COLLECTION_HASH); + replaceDraft( + 'viewFilterGroups', + data.flatViewFilterGroups, + MOCKED_COLLECTION_HASH, + ); + replaceDraft( + 'viewFieldGroups', + data.flatViewFieldGroups, + MOCKED_COLLECTION_HASH, + ); + replaceDraft( + 'navigationMenuItems', + data.navigationMenuItems, + MOCKED_COLLECTION_HASH, + ); + + applyChanges(); + }, + [replaceDraft, applyChanges, resetMetadataStore], + ); + + const isAlreadyMocked = useCallback(() => { + const entry = store.get( + metadataStoreState.atomFamily('objectMetadataItems'), + ); + + return ( + entry.status === 'up-to-date' && + entry.currentCollectionHash === MOCKED_COLLECTION_HASH + ); + }, [store]); + + const loadMockedMetadataAtomic = useCallback(async () => { + if (isAlreadyMocked()) { + return; + } + + const data = await preloadMockedMetadata(); + applyMockedMetadata(data); + }, [applyMockedMetadata, isAlreadyMocked]); + + return { applyMockedMetadata, loadMockedMetadataAtomic }; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMockedMinimalMetadata.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMockedMinimalMetadata.ts deleted file mode 100644 index 69abdd8be2..0000000000 --- a/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMockedMinimalMetadata.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; -import { splitObjectMetadataGqlResponse } from '@/metadata-store/utils/splitObjectMetadataGqlResponse'; -import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated'; -import { useCallback } from 'react'; - -const MOCKED_COLLECTION_HASH = 'mocked'; - -export const useLoadMockedMinimalMetadata = () => { - const { replaceDraft, applyChanges, resetMetadataStore } = useMetadataStore(); - - const loadMockedMinimalMetadata = useCallback(async () => { - const [ - { mockedStandardObjectMetadataQueryResult }, - { mockedViews }, - { mockedNavigationMenuItems }, - ] = await Promise.all([ - import( - '~/testing/mock-data/generated/metadata/objects/mock-objects-metadata' - ), - import('~/testing/mock-data/generated/metadata/views/mock-views-data'), - import( - '~/testing/mock-data/generated/metadata/navigation-menu-items/mock-navigation-menu-items-data' - ), - ]); - - // Reset after async imports so reset + replaceDraft + applyChanges - // run in the same synchronous block (React 18 batches them into one render), - // avoiding a window where the store appears empty to subscribers. - resetMetadataStore(); - - const { flatObjects, flatFields, flatIndexes } = - splitObjectMetadataGqlResponse(mockedStandardObjectMetadataQueryResult); - - replaceDraft('objectMetadataItems', flatObjects, MOCKED_COLLECTION_HASH); - replaceDraft('fieldMetadataItems', flatFields, MOCKED_COLLECTION_HASH); - replaceDraft('indexMetadataItems', flatIndexes, MOCKED_COLLECTION_HASH); - - const { - flatViews, - flatViewFields, - flatViewFilters, - flatViewSorts, - flatViewGroups, - flatViewFilterGroups, - flatViewFieldGroups, - } = splitViewWithRelated(mockedViews); - - replaceDraft('views', flatViews, MOCKED_COLLECTION_HASH); - replaceDraft('viewFields', flatViewFields, MOCKED_COLLECTION_HASH); - replaceDraft('viewFilters', flatViewFilters, MOCKED_COLLECTION_HASH); - replaceDraft('viewSorts', flatViewSorts, MOCKED_COLLECTION_HASH); - replaceDraft('viewGroups', flatViewGroups, MOCKED_COLLECTION_HASH); - replaceDraft( - 'viewFilterGroups', - flatViewFilterGroups, - MOCKED_COLLECTION_HASH, - ); - replaceDraft( - 'viewFieldGroups', - flatViewFieldGroups, - MOCKED_COLLECTION_HASH, - ); - - replaceDraft( - 'navigationMenuItems', - mockedNavigationMenuItems, - MOCKED_COLLECTION_HASH, - ); - - applyChanges(); - }, [replaceDraft, applyChanges, resetMetadataStore]); - - return { loadMockedMinimalMetadata }; -}; diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useLoadStaleMetadataEntities.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadStaleMetadataEntities.ts index 0c1d9131e6..badcf357b8 100644 --- a/packages/twenty-front/src/modules/metadata-store/hooks/useLoadStaleMetadataEntities.ts +++ b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadStaleMetadataEntities.ts @@ -1,4 +1,4 @@ -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { type MetadataEntityKey } from '@/metadata-store/states/metadataStoreState'; import { splitObjectMetadataGqlResponse } from '@/metadata-store/utils/splitObjectMetadataGqlResponse'; import { splitPageLayoutWithRelated } from '@/metadata-store/utils/splitPageLayoutWithRelated'; @@ -53,7 +53,7 @@ const hasOverlap = ( export const useLoadStaleMetadataEntities = () => { const client = useApolloClient(); const store = useStore(); - const { replaceDraft, applyChanges } = useMetadataStore(); + const { replaceDraft, applyChanges } = useUpdateMetadataStoreDraft(); const loadStaleMetadataEntities = useCallback( async (staleEntityKeys: MetadataEntityKey[]) => { diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useMetadataStore.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useUpdateMetadataStoreDraft.ts similarity index 99% rename from packages/twenty-front/src/modules/metadata-store/hooks/useMetadataStore.ts rename to packages/twenty-front/src/modules/metadata-store/hooks/useUpdateMetadataStoreDraft.ts index 7b1059a272..070373d357 100644 --- a/packages/twenty-front/src/modules/metadata-store/hooks/useMetadataStore.ts +++ b/packages/twenty-front/src/modules/metadata-store/hooks/useUpdateMetadataStoreDraft.ts @@ -56,7 +56,7 @@ const changeMetadataEntityAsUpToDate = ( }); }; -export const useMetadataStore = () => { +export const useUpdateMetadataStoreDraft = () => { const store = useStore(); const replaceDraft = useCallback( diff --git a/packages/twenty-front/src/modules/metadata-store/states/metadataLoadedVersionState.ts b/packages/twenty-front/src/modules/metadata-store/states/metadataLoadedVersionState.ts new file mode 100644 index 0000000000..77ce74f677 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/states/metadataLoadedVersionState.ts @@ -0,0 +1,6 @@ +import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState'; + +export const metadataLoadedVersionState = createAtomState({ + key: 'metadataLoadedVersionState', + defaultValue: 0, +}); diff --git a/packages/twenty-front/src/modules/metadata-store/utils/preloadMockedMetadata.ts b/packages/twenty-front/src/modules/metadata-store/utils/preloadMockedMetadata.ts new file mode 100644 index 0000000000..231cd5183e --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/utils/preloadMockedMetadata.ts @@ -0,0 +1,72 @@ +import { splitObjectMetadataGqlResponse } from '@/metadata-store/utils/splitObjectMetadataGqlResponse'; +import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated'; + +import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem'; +import { type FlatIndexMetadataItem } from '@/metadata-store/types/FlatIndexMetadataItem'; +import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem'; +import { type FlatView } from '@/metadata-store/types/FlatView'; +import { type FlatViewField } from '@/metadata-store/types/FlatViewField'; +import { type FlatViewFieldGroup } from '@/metadata-store/types/FlatViewFieldGroup'; +import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter'; +import { type FlatViewFilterGroup } from '@/metadata-store/types/FlatViewFilterGroup'; +import { type FlatViewGroup } from '@/metadata-store/types/FlatViewGroup'; +import { type FlatViewSort } from '@/metadata-store/types/FlatViewSort'; +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +export type PreloadedMockedMetadata = { + flatObjects: FlatObjectMetadataItem[]; + flatFields: FlatFieldMetadataItem[]; + flatIndexes: FlatIndexMetadataItem[]; + flatViews: FlatView[]; + flatViewFields: FlatViewField[]; + flatViewFilters: FlatViewFilter[]; + flatViewSorts: FlatViewSort[]; + flatViewGroups: FlatViewGroup[]; + flatViewFilterGroups: FlatViewFilterGroup[]; + flatViewFieldGroups: FlatViewFieldGroup[]; + navigationMenuItems: NavigationMenuItem[]; +}; + +export const preloadMockedMetadata = + async (): Promise => { + const [ + { mockedStandardObjectMetadataQueryResult }, + { mockedViews }, + { mockedNavigationMenuItems }, + ] = await Promise.all([ + import( + '~/testing/mock-data/generated/metadata/objects/mock-objects-metadata' + ), + import('~/testing/mock-data/generated/metadata/views/mock-views-data'), + import( + '~/testing/mock-data/generated/metadata/navigation-menu-items/mock-navigation-menu-items-data' + ), + ]); + + const { flatObjects, flatFields, flatIndexes } = + splitObjectMetadataGqlResponse(mockedStandardObjectMetadataQueryResult); + + const { + flatViews, + flatViewFields, + flatViewFilters, + flatViewSorts, + flatViewGroups, + flatViewFilterGroups, + flatViewFieldGroups, + } = splitViewWithRelated(mockedViews); + + return { + flatObjects, + flatFields, + flatIndexes, + flatViews, + flatViewFields, + flatViewFilters, + flatViewSorts, + flatViewGroups, + flatViewFilterGroups, + flatViewFieldGroups, + navigationMenuItems: mockedNavigationMenuItems, + }; + }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useCreateNavigationMenuItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useCreateNavigationMenuItem.ts index ece747fb59..96a133cbb1 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useCreateNavigationMenuItem.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useCreateNavigationMenuItem.ts @@ -8,7 +8,7 @@ import { type NavigationMenuItem, } from '~/generated-metadata/graphql'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; @@ -38,7 +38,7 @@ export const useCreateNavigationMenuItem = () => { const { navigationMenuItems, currentWorkspaceMemberId } = useNavigationMenuItemsData(); const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); - const { addToDraft, applyChanges } = useMetadataStore(); + const { addToDraft, applyChanges } = useUpdateMetadataStoreDraft(); const [createNavigationMenuItemMutation] = useMutation( CreateNavigationMenuItemDocument, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem.ts index c6a2d670db..2e25bd0ee7 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem.ts @@ -1,10 +1,10 @@ import { useMutation } from '@apollo/client/react'; import { DeleteNavigationMenuItemDocument } from '~/generated-metadata/graphql'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; export const useDeleteNavigationMenuItem = () => { - const { removeFromDraft, applyChanges } = useMetadataStore(); + const { removeFromDraft, applyChanges } = useUpdateMetadataStoreDraft(); const [deleteNavigationMenuItemMutation] = useMutation( DeleteNavigationMenuItemDocument, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useRemoveNavigationMenuItemByTargetRecordId.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useRemoveNavigationMenuItemByTargetRecordId.ts index e3ad891dbf..dee5812d44 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useRemoveNavigationMenuItemByTargetRecordId.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useRemoveNavigationMenuItemByTargetRecordId.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { metadataStoreState } from '@/metadata-store/states/metadataStoreState'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { @@ -14,7 +14,7 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => { const store = useStore(); const apolloCoreClient = useApolloCoreClient(); const cache = apolloCoreClient.cache; - const { replaceDraft, applyChanges } = useMetadataStore(); + const { replaceDraft, applyChanges } = useUpdateMetadataStoreDraft(); const removeNavigationMenuItemsByTargetRecordIds = useCallback( (targetRecordIds: string[]) => { diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem.ts index 7a953d8ca2..26cfa6328e 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem.ts @@ -7,10 +7,10 @@ import { UpdateNavigationMenuItemDocument, } from '~/generated-metadata/graphql'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; export const useUpdateNavigationMenuItem = () => { - const { addToDraft, applyChanges } = useMetadataStore(); + const { addToDraft, applyChanges } = useUpdateMetadataStoreDraft(); const [updateNavigationMenuItemMutation] = useMutation( UpdateNavigationMenuItemDocument, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useOptimisticRemoveNavigationMenuItemsByViewId.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useOptimisticRemoveNavigationMenuItemsByViewId.ts index 8f843d6778..4134aa47be 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useOptimisticRemoveNavigationMenuItemsByViewId.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useOptimisticRemoveNavigationMenuItemsByViewId.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { metadataStoreState } from '@/metadata-store/states/metadataStoreState'; import { type NavigationMenuItem } from '~/generated-metadata/graphql'; import { isDefined } from 'twenty-shared/utils'; @@ -8,7 +8,7 @@ import { useStore } from 'jotai'; export const useOptimisticRemoveNavigationMenuItemsByViewId = () => { const store = useStore(); - const { replaceDraft, applyChanges } = useMetadataStore(); + const { replaceDraft, applyChanges } = useUpdateMetadataStoreDraft(); const removeNavigationMenuItemsByViewIds = useCallback( (viewIds: string[]) => { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts index e305a25b38..7b47b9de4b 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts @@ -5,7 +5,7 @@ import { } from '~/generated-metadata/graphql'; import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem'; import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -21,7 +21,7 @@ export const useCreateOneFieldMetadataItem = () => { const { handleMetadataError } = useMetadataErrorHandler(); const { enqueueErrorSnackBar } = useSnackBar(); - const { addToDraft, applyChanges } = useMetadataStore(); + const { addToDraft, applyChanges } = useUpdateMetadataStoreDraft(); const createOneFieldMetadataItem = async ( input: CreateFieldInput, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts index 5516f8de79..dd52c7a77b 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts @@ -7,7 +7,7 @@ import { } from '~/generated-metadata/graphql'; import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem'; import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem'; import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated'; @@ -26,7 +26,8 @@ export const useCreateOneObjectMetadataItem = () => { const client = useApolloClient(); const { handleMetadataError } = useMetadataErrorHandler(); const { enqueueErrorSnackBar } = useSnackBar(); - const { addToDraft, replaceDraft, applyChanges } = useMetadataStore(); + const { addToDraft, replaceDraft, applyChanges } = + useUpdateMetadataStoreDraft(); const createOneObjectMetadataItem = async ( input: CreateObjectInput, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts index f931714844..8c9e013514 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts @@ -3,7 +3,7 @@ import { DeleteOneFieldMetadataItemDocument } from '~/generated-metadata/graphql import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; import { recordIndexGroupAggregateFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateFieldMetadataItemComponentState'; import { recordIndexGroupAggregateOperationComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateOperationComponentState'; @@ -22,7 +22,7 @@ export const useDeleteOneFieldMetadataItem = () => { const { handleMetadataError } = useMetadataErrorHandler(); const { enqueueErrorSnackBar } = useSnackBar(); - const { removeFromDraft, applyChanges } = useMetadataStore(); + const { removeFromDraft, applyChanges } = useUpdateMetadataStoreDraft(); const setRecordIndexGroupAggregateOperation = useSetAtomComponentState( recordIndexGroupAggregateOperationComponentState, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts index aa94d7ecb1..b4fb40b715 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts @@ -2,7 +2,7 @@ import { useMutation } from '@apollo/client/react'; import { DeleteOneObjectMetadataItemDocument } from '~/generated-metadata/graphql'; import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { CombinedGraphQLErrors } from '@apollo/client/errors'; @@ -16,7 +16,7 @@ export const useDeleteOneObjectMetadataItem = () => { const { handleMetadataError } = useMetadataErrorHandler(); const { enqueueErrorSnackBar } = useSnackBar(); - const { removeFromDraft, applyChanges } = useMetadataStore(); + const { removeFromDraft, applyChanges } = useUpdateMetadataStoreDraft(); const deleteOneObjectMetadataItem = async ( idToDelete: string, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts index fccf6a91e7..adc535663f 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts @@ -5,7 +5,7 @@ import { } from '~/generated-metadata/graphql'; import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem'; import { lastFieldMetadataItemUpdateState } from '@/object-metadata/states/lastFieldMetadataItemUpdateState'; import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; @@ -25,7 +25,7 @@ export const useUpdateOneFieldMetadataItem = () => { const { handleMetadataError } = useMetadataErrorHandler(); const { enqueueErrorSnackBar } = useSnackBar(); - const { updateInDraft, applyChanges } = useMetadataStore(); + const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft(); const setLastFieldMetadataItemUpdate = useSetAtomState( lastFieldMetadataItemUpdateState, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts index e9ac7039b5..a0d1a295a2 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts @@ -5,7 +5,7 @@ import { } from '~/generated-metadata/graphql'; import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem'; import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -22,7 +22,7 @@ export const useUpdateOneObjectMetadataItem = () => { const { handleMetadataError } = useMetadataErrorHandler(); const { enqueueErrorSnackBar } = useSnackBar(); - const { updateInDraft, applyChanges } = useMetadataStore(); + const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft(); const updateOneObjectMetadataItem = async ({ idToUpdate, diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx index cde6406e5a..c4fe4a5707 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx @@ -1,7 +1,7 @@ import { type Meta, type StoryObj } from '@storybook/react-vite'; import { useEffect } from 'react'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { splitCompositeObjectMetadataItems } from '@/metadata-store/utils/splitCompositeObjectMetadataItems'; import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage'; @@ -53,7 +53,7 @@ const RelationFieldValueSetterEffect = () => { 'recordTableId', ); - const { replaceDraft, applyChanges } = useMetadataStore(); + const { replaceDraft, applyChanges } = useUpdateMetadataStoreDraft(); useEffect(() => { setRecordStore(mockPerformance.entityValue); diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx index 4eeba7bd84..3bcf245e0b 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx @@ -2,14 +2,15 @@ import { useEffect } from 'react'; import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; +import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions'; import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState'; +import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue'; import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; +import { isDefined } from 'twenty-shared/utils'; import { useInitViewBar } from '@/views/hooks/useInitViewBar'; type SignInBackgroundMockContainerEffectProps = { @@ -36,18 +37,19 @@ export const SignInBackgroundMockContainerEffect = ({ recordTableId, ); - const { objectNameSingular } = useObjectNameSingularFromPlural({ - objectNamePlural, - }); - - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); + const objectMetadataItem = useAtomFamilySelectorValue( + objectMetadataItemFamilySelector, + { objectName: objectNamePlural, objectNameType: 'plural' }, + ); const { setAvailableFieldDefinitions, setViewObjectMetadataId } = useInitViewBar(viewId); useEffect(() => { + if (!isDefined(objectMetadataItem)) { + return; + } + setViewObjectMetadataId?.(objectMetadataItem.id); setAvailableFieldDefinitions?.(SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS); diff --git a/packages/twenty-front/src/modules/sse-db-event/components/SSEQuerySubscribeEffect.tsx b/packages/twenty-front/src/modules/sse-db-event/components/SSEQuerySubscribeEffect.tsx index fc4595af47..2ce68bf759 100644 --- a/packages/twenty-front/src/modules/sse-db-event/components/SSEQuerySubscribeEffect.tsx +++ b/packages/twenty-front/src/modules/sse-db-event/components/SSEQuerySubscribeEffect.tsx @@ -88,20 +88,21 @@ export const SSEQuerySubscribeEffect = () => { } catch (error) { if (CombinedGraphQLErrors.is(error)) { const subCode = error.errors[0]?.extensions?.subCode; + const code = error.errors[0]?.extensions?.code; - switch (subCode) { - case 'EVENT_STREAM_DOES_NOT_EXIST': - case 'EVENT_STREAM_ALREADY_EXISTS': { - store.set(activeQueryListenersState.atom, []); - store.set(shouldDestroyEventStreamState.atom, true); - return; - } - default: { - throw new Error( - `Unhandled error for event stream: ${error.message}`, - ); - } + const isRecoverable = + subCode === 'EVENT_STREAM_DOES_NOT_EXIST' || + subCode === 'EVENT_STREAM_ALREADY_EXISTS' || + code === 'UNAUTHENTICATED' || + code === 'FORBIDDEN'; + + if (isRecoverable) { + store.set(activeQueryListenersState.atom, []); + store.set(shouldDestroyEventStreamState.atom, true); + return; } + + throw new Error(`Unhandled error for event stream: ${error.message}`); } } diff --git a/packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts b/packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts index c47d1afc66..9f508eb935 100644 --- a/packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts +++ b/packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts @@ -1,13 +1,10 @@ +import { clearAllSessionLocalStorageKeys } from '@/auth/utils/clearAllSessionLocalStorageKeys'; import { createStore } from 'jotai'; export let jotaiStore = createStore(); export const resetJotaiStore = () => { - try { - localStorage.clear(); - } catch { - // localStorage may be unavailable in some environments - } + clearAllSessionLocalStorageKeys(); jotaiStore = createStore(); diff --git a/packages/twenty-front/src/modules/views/utils/resolveViewNamePlaceholders.ts b/packages/twenty-front/src/modules/views/utils/resolveViewNamePlaceholders.ts index c5ed486072..7e32bbad9e 100644 --- a/packages/twenty-front/src/modules/views/utils/resolveViewNamePlaceholders.ts +++ b/packages/twenty-front/src/modules/views/utils/resolveViewNamePlaceholders.ts @@ -2,11 +2,11 @@ import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMe import { isDefined } from 'twenty-shared/utils'; export const resolveViewNamePlaceholders = ( - viewName: string, + viewName: string | undefined, objectMetadataItem: FlatObjectMetadataItem | undefined, ): string => { - if (!isDefined(objectMetadataItem)) { - return viewName; + if (!isDefined(viewName) || !isDefined(objectMetadataItem)) { + return viewName ?? ''; } return viewName diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx index 85bf3c6463..32105f7202 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx @@ -7,7 +7,7 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale'; import { Select } from '@/ui/input/components/Select'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidateMetadataStore'; import { useStore } from 'jotai'; import { useLingui } from '@lingui/react/macro'; import { enUS } from 'date-fns/locale'; @@ -32,7 +32,7 @@ export const LocalePicker = () => { ); const { updateOneRecord } = useUpdateOneRecord(); - const { resetMetadataStore } = useMetadataStore(); + const { invalidateMetadataStore } = useInvalidateMetadataStore(); const updateWorkspaceMember = async (changedFields: any) => { if (!currentWorkspaceMember?.id) { @@ -73,7 +73,7 @@ export const LocalePicker = () => { // oxlint-disable-next-line no-console console.log('Failed to save locale to localStorage:', error); } - resetMetadataStore(); + invalidateMetadataStore(); }; const unsortedLocaleOptions: Array<{ diff --git a/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx b/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx index 283f22e7e9..13b3763dd1 100644 --- a/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx @@ -7,7 +7,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider'; -import { useLoadMockedMinimalMetadata } from '@/metadata-store/hooks/useLoadMockedMinimalMetadata'; +import { useLoadMockedMetadata } from '@/metadata-store/hooks/useLoadMockedMetadata'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode'; import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; @@ -22,7 +22,7 @@ export const ObjectMetadataItemsDecorator: Decorator = (Story) => { const setCurrentUser = useSetAtomState(currentUserState); const setCurrentUserWorkspace = useSetAtomState(currentUserWorkspaceState); - const { loadMockedMinimalMetadata } = useLoadMockedMinimalMetadata(); + const { loadMockedMetadataAtomic } = useLoadMockedMetadata(); useEffect(() => { setCurrentWorkspaceMember( @@ -32,12 +32,12 @@ export const ObjectMetadataItemsDecorator: Decorator = (Story) => { ); setCurrentUser(mockedUserData); setCurrentUserWorkspace(mockedUserData.currentUserWorkspace); - loadMockedMinimalMetadata(); + loadMockedMetadataAtomic(); }, [ setCurrentUser, setCurrentWorkspaceMember, setCurrentUserWorkspace, - loadMockedMinimalMetadata, + loadMockedMetadataAtomic, ]); return ( diff --git a/packages/twenty-front/src/testing/decorators/WorkflowStepDecorator.tsx b/packages/twenty-front/src/testing/decorators/WorkflowStepDecorator.tsx index 052309cb7b..4baac3476e 100644 --- a/packages/twenty-front/src/testing/decorators/WorkflowStepDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/WorkflowStepDecorator.tsx @@ -1,6 +1,6 @@ import { sidePanelWorkflowIdComponentState } from '@/side-panel/pages/workflow/states/sidePanelWorkflowIdComponentState'; import { SidePanelPageComponentInstanceContext } from '@/side-panel/states/contexts/SidePanelPageComponentInstanceContext'; -import { useLoadMockedMinimalMetadata } from '@/metadata-store/hooks/useLoadMockedMinimalMetadata'; +import { useLoadMockedMetadata } from '@/metadata-store/hooks/useLoadMockedMetadata'; import { flowComponentState } from '@/workflow/states/flowComponentState'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; import { workflowVisualizerWorkflowRunIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowRunIdComponentState'; @@ -24,7 +24,7 @@ export const WorkflowStepDecorator: Decorator = (Story) => { const workflowVersion = mockedWorkflowVersion as WorkflowVersion; const { populateStepsOutputSchema } = useStepsOutputSchema(); - const { loadMockedMinimalMetadata } = useLoadMockedMinimalMetadata(); + const { loadMockedMetadataAtomic } = useLoadMockedMetadata(); const [ready, setReady] = useState(false); @@ -32,7 +32,7 @@ export const WorkflowStepDecorator: Decorator = (Story) => { useEffect(() => { const setup = async () => { - await loadMockedMinimalMetadata(); + await loadMockedMetadataAtomic(); store.set( workflowVisualizerWorkflowIdComponentState.atomFamily({ @@ -80,7 +80,7 @@ export const WorkflowStepDecorator: Decorator = (Story) => { setup(); }, [ - loadMockedMinimalMetadata, + loadMockedMetadataAtomic, populateStepsOutputSchema, workflowVersion, store, diff --git a/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx b/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx index 134db13cbe..0c34453e72 100644 --- a/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx +++ b/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx @@ -1,6 +1,6 @@ import { type ReactNode, useEffect, useState } from 'react'; -import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { splitCompositeObjectMetadataItems } from '@/metadata-store/utils/splitCompositeObjectMetadataItems'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock'; @@ -12,7 +12,7 @@ export const JestObjectMetadataItemSetter = ({ children: ReactNode; objectMetadataItems?: EnrichedObjectMetadataItem[]; }) => { - const { replaceDraft, applyChanges } = useMetadataStore(); + const { replaceDraft, applyChanges } = useUpdateMetadataStoreDraft(); const [isLoaded, setIsLoaded] = useState(false); useEffect(() => {