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(() => {