Various SDK improvements (#18115)

## Summary

- **Refactor frontend metadata loading architecture**: Split the
monolithic `EagerMetadataLoadEffect` into focused provider effects
(`UserMetadataProviderEffect`, `ObjectMetadataProviderEffect`,
`ViewMetadataProviderEffect`) orchestrated by `MetadataProviderEffects`.
Replaced `UserProvider` + `ObjectMetadataItemsProvider` with a single
`MetadataGater` that gates rendering on `isAppMetadataReadyState`. The
metadata store now validates view-object consistency before promoting
views, and `updateDraft` skips no-op updates via deep equality checks.

- **SDK CLI improvements**: Added `app:typecheck` command, improved
error handling in API sync (extracts GraphQL error messages), added
`serializeError` utility for human-readable error output, added `error`
file status to dev mode orchestrator with UI support, and fixed
ClickHouse migration/seed commands to use `transpile-only`.
This commit is contained in:
Charles Bochet
2026-02-23 19:57:02 +01:00
committed by GitHub
parent ccddd105d8
commit 0d4fe4575b
38 changed files with 675 additions and 450 deletions
@@ -1,6 +1,9 @@
import { AgentChatProvider } from '@/ai/components/AgentChatProvider';
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
import { MetadataGater } from '@/metadata-store/components/MetadataGater';
import { IsAppMetadataReadyEffect } from '@/metadata-store/effect-components/IsAppMetadataReadyEffect';
import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider';
import { MetadataProviderInitialEffects } from '@/metadata-store/effect-components/MetadataProviderInitialEffects';
import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect';
import { AuthProvider } from '@/auth/components/AuthProvider';
import { CaptchaProvider } from '@/captcha/components/CaptchaProvider';
@@ -13,7 +16,7 @@ import { ErrorMessageEffect } from '@/error-handler/components/ErrorMessageEffec
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
import { HeadlessFrontComponentMountRoot } from '@/front-components/components/HeadlessFrontComponentMountRoot';
import { ApolloCoreProvider } from '@/object-metadata/components/ApolloCoreProvider';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
import { SSEProvider } from '@/sse-db-event/components/SSEProvider';
import { SupportChatEffect } from '@/support/components/SupportChatEffect';
@@ -25,10 +28,7 @@ import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider';
import { UserThemeProviderEffect } from '@/ui/theme/components/UserThemeProviderEffect';
import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { EagerMetadataLoadEffect } from '@/users/components/EagerMetadataLoadEffect';
import { LazyMetadataLoadEffect } from '@/users/components/LazyMetadataLoadEffect';
import { MetadataProviderEffect } from '@/users/components/MetadataProviderEffect';
import { UserProvider } from '@/users/components/UserProvider';
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
import { StrictMode } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
@@ -42,19 +42,19 @@ export const AppRouterProviders = () => {
<ApolloProvider>
<BaseThemeProvider>
<ClientConfigProviderEffect />
<MetadataProviderEffect />
<EagerMetadataLoadEffect />
<MetadataProviderInitialEffects />
<LazyMetadataLoadEffect />
<IsAppMetadataReadyEffect />
<WorkspaceProviderEffect />
<ClientConfigProvider>
<CaptchaProvider>
<ChromeExtensionSidecarEffect />
<ChromeExtensionSidecarProvider>
<UserProvider>
<MetadataGater>
<AuthProvider>
<ApolloCoreProvider>
<SSEProvider>
<ObjectMetadataItemsProvider>
<PreComputedChipGeneratorsProvider>
<PrefetchDataProvider>
<UserThemeProviderEffect />
<SnackBarProvider>
@@ -81,11 +81,11 @@ export const AppRouterProviders = () => {
<SupportChatEffect />
</PrefetchDataProvider>
<PageChangeEffect />
</ObjectMetadataItemsProvider>
</PreComputedChipGeneratorsProvider>
</SSEProvider>
</ApolloCoreProvider>
</AuthProvider>
</UserProvider>
</MetadataGater>
</ChromeExtensionSidecarProvider>
</CaptchaProvider>
</ClientConfigProvider>
@@ -1,84 +0,0 @@
import {
metadataStoreState,
type MetadataKey,
type MetadataLoadEntry,
} from '@/app/states/metadataStoreState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
const EMPTY_ENTRY: MetadataLoadEntry = {
current: [],
draft: [],
status: 'empty',
};
// Views reference objects via objectMetadataId — both drafts must agree on IDs.
const areDraftsConsistent = (
objectsDraft: object[],
viewsDraft: object[],
): boolean => {
const objectIds = new Set(
objectsDraft.map((item) => (item as { id: string }).id),
);
return viewsDraft.every((view) =>
objectIds.has((view as { objectMetadataId: string }).objectMetadataId),
);
};
export const useMetadataStore = () => {
const store = useStore();
const updateDraft = useCallback(
(key: MetadataKey, data: object[]) => {
store.set(metadataStoreState.atomFamily(key), (prev) => ({
...prev,
draft: data,
status: 'draft_pending' as const,
}));
},
[store],
);
// Validates consistency then atomically promotes all 'draft_pending' drafts
// to current. If objects and views are inconsistent (e.g. mocked objects with
// real views), resets the stale entries back to 'empty' so effects re-fetch.
const applyChanges = useCallback(() => {
const objectsEntry = store.get(metadataStoreState.atomFamily('objects'));
const viewsEntry = store.get(metadataStoreState.atomFamily('views'));
if (
objectsEntry.status === 'draft_pending' &&
viewsEntry.status === 'draft_pending' &&
!areDraftsConsistent(objectsEntry.draft, viewsEntry.draft)
) {
store.set(metadataStoreState.atomFamily('objects'), EMPTY_ENTRY);
store.set(metadataStoreState.atomFamily('views'), EMPTY_ENTRY);
return;
}
const allKeys: MetadataKey[] = [
'objects',
'views',
'pageLayouts',
'logicFunctions',
];
for (const key of allKeys) {
const entry = store.get(metadataStoreState.atomFamily(key));
if (entry.status === 'draft_pending') {
store.set(metadataStoreState.atomFamily(key), {
current: entry.draft,
draft: [],
status: 'loaded',
});
}
}
store.set(shouldAppBeLoadingState.atom, false);
}, [store]);
return { updateDraft, applyChanges };
};
@@ -1,12 +0,0 @@
import { metadataStoreState } from '@/app/states/metadataStoreState';
import { createSelectorV2 } from '@/ui/utilities/state/jotai/utils/createSelectorV2';
export const isAppLoadingState = createSelectorV2<boolean>({
key: 'isAppLoadingState',
get: ({ get }) => {
const objectsEntry = get(metadataStoreState, 'objects');
const viewsEntry = get(metadataStoreState, 'views');
return objectsEntry.status !== 'loaded' || viewsEntry.status !== 'loaded';
},
});
@@ -35,7 +35,7 @@ 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 { metadataStoreState } from '@/app/states/metadataStoreState';
import { useReloadWorkspaceMetadata } from '@/metadata-store/hooks/useReloadWorkspaceMetadata';
import { lastAuthenticatedMethodState } from '@/auth/states/lastAuthenticatedMethodState';
import { loginTokenState } from '@/auth/states/loginTokenState';
import {
@@ -58,8 +58,6 @@ import { useOrigin } from '@/domain-manager/hooks/useOrigin';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { sseClientState } from '@/sse-db-event/states/sseClientState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
@@ -89,7 +87,8 @@ export const useAuth = () => {
);
const { loadCurrentUser } = useLoadCurrentUser();
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { reloadWorkspaceMetadata, resetToMockedMetadata } =
useReloadWorkspaceMetadata();
const { createWorkspace } = useSignUpInNewWorkspace();
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
@@ -123,7 +122,6 @@ export const useAuth = () => {
const [, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
const clearSession = useRecoilCallback(
({ snapshot }) =>
@@ -161,26 +159,6 @@ export const useAuth = () => {
jotaiStore.set(workspaceAuthProvidersState.atom, authProvidersValue);
jotaiStore.set(workspacePublicDataState.atom, workspacePublicDataValue);
jotaiStore.set(metadataStoreState.atomFamily('objects'), {
current: [],
draft: [],
status: 'empty',
});
jotaiStore.set(metadataStoreState.atomFamily('views'), {
current: [],
draft: [],
status: 'empty',
});
jotaiStore.set(metadataStoreState.atomFamily('pageLayouts'), {
current: [],
draft: [],
status: 'empty',
});
jotaiStore.set(metadataStoreState.atomFamily('logicFunctions'), {
current: [],
draft: [],
status: 'empty',
});
jotaiStore.set(domainConfigurationState.atom, domainConfigurationValue);
jotaiStore.set(
isCaptchaScriptLoadedState.atom,
@@ -209,14 +187,14 @@ export const useAuth = () => {
await client.clearStore();
setLastAuthenticateWorkspaceDomain(null);
await loadMockedObjectMetadataItems();
await resetToMockedMetadata();
navigate(AppPath.SignInUp);
},
[
goToRecoilSnapshot,
client,
setLastAuthenticateWorkspaceDomain,
loadMockedObjectMetadataItems,
resetToMockedMetadata,
navigate,
],
);
@@ -344,19 +322,15 @@ export const useAuth = () => {
const handleLoadWorkspaceAfterAuthentication = useCallback(
async (authTokens: AuthTokenPair) => {
handleSetAuthTokens(authTokens);
setIsAppEffectRedirectEnabled(false);
// TODO: We can't parallelize this yet because when loadCurrentUSer is loaded
// then UserProvider updates its children and PrefetchDataProvider is then triggered
// which requires the correct metadata to be loaded (not the mocks)
await loadCurrentUser();
await refreshObjectMetadataItems();
await reloadWorkspaceMetadata();
},
[
loadCurrentUser,
handleSetAuthTokens,
refreshObjectMetadataItems,
reloadWorkspaceMetadata,
setIsAppEffectRedirectEnabled,
],
);
@@ -1,4 +1,4 @@
import { metadataStoreState } from '@/app/states/metadataStoreState';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { MainContextStoreProviderEffect } from '@/context-store/components/MainContextStoreProviderEffect';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
@@ -1,8 +1,9 @@
import React from 'react';
import { isAppLoadingState } from '@/app/states/isAppLoadingState';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { UserContext } from '@/users/contexts/UserContext';
import { useLocation } from 'react-router-dom';
@@ -10,23 +11,28 @@ import { AppPath } from 'twenty-shared/types';
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
export const UserProvider = ({ children }: React.PropsWithChildren) => {
const isAppLoading = useRecoilValueV2(isAppLoadingState);
export const MetadataGater = ({ children }: React.PropsWithChildren) => {
const isAppMetadataReady = useRecoilValueV2(isAppMetadataReadyState);
const objectMetadataItems = useRecoilValueV2(objectMetadataItemsState);
const isLoggedIn = useIsLogged();
const location = useLocation();
const { dateFormat, timeFormat, timeZone } = useDateTimeFormat();
const shouldShowLoader =
isAppLoading &&
isLoggedIn &&
!isMatchingLocation(location, AppPath.Verify) &&
!isMatchingLocation(location, AppPath.VerifyEmail) &&
!isMatchingLocation(location, AppPath.CreateWorkspace);
const isOnExcludedPath =
isMatchingLocation(location, AppPath.Verify) ||
isMatchingLocation(location, AppPath.VerifyEmail) ||
isMatchingLocation(location, AppPath.CreateWorkspace);
return shouldShowLoader ? (
<UserOrMetadataLoader />
) : (
const shouldShowLoader =
(!isAppMetadataReady && isLoggedIn && !isOnExcludedPath) ||
objectMetadataItems.length === 0;
if (shouldShowLoader) {
return <UserOrMetadataLoader />;
}
return (
<UserContext.Provider
value={{
dateFormat,
@@ -0,0 +1,54 @@
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { useFamilyRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilyRecoilValueV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
export const IsAppMetadataReadyEffect = () => {
const isLoggedIn = useIsLogged();
const currentUser = useRecoilValueV2(currentUserState);
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const objectsEntry = useFamilyRecoilValueV2(metadataStoreState, 'objects');
const viewsEntry = useFamilyRecoilValueV2(metadataStoreState, 'views');
const setIsAppMetadataReady = useSetRecoilStateV2(isAppMetadataReadyState);
console.log('objectsEntry', objectsEntry);
console.log('viewsEntry', viewsEntry);
console.log('isLoggedIn', isLoggedIn);
console.log('currentUser', currentUser);
console.log('currentWorkspace', currentWorkspace);
console.log('setIsAppMetadataReady', setIsAppMetadataReady);
useEffect(() => {
const hasActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace);
const areObjectsLoaded = objectsEntry.status === 'loaded';
const areViewsLoaded = viewsEntry.status === 'loaded';
if (!areObjectsLoaded) {
setIsAppMetadataReady(false);
return;
}
const isReady =
!isLoggedIn ||
(isDefined(currentUser) && (!hasActiveWorkspace || areViewsLoaded));
setIsAppMetadataReady(isReady);
}, [
isLoggedIn,
currentUser,
currentWorkspace,
objectsEntry.status,
viewsEntry.status,
setIsAppMetadataReady,
]);
return null;
};
@@ -0,0 +1,11 @@
import { ObjectMetadataProviderInitialEffect } from '@/metadata-store/effect-components/ObjectMetadataProviderInitialEffect';
import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect';
import { ViewMetadataProviderInitialEffect } from '@/metadata-store/effect-components/ViewMetadataProviderInitialEffect';
export const MetadataProviderInitialEffects = () => (
<>
<UserMetadataProviderInitialEffect />
<ObjectMetadataProviderInitialEffect />
<ViewMetadataProviderInitialEffect />
</>
);
@@ -1,45 +1,54 @@
import { currentUserState } from '@/auth/states/currentUserState';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useEffect, useState } from 'react';
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useStore } from 'jotai';
import { useEffect, useState } from 'react';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const ObjectMetadataItemsLoadEffect = () => {
const currentUser = useRecoilValueV2(currentUserState);
export const ObjectMetadataProviderInitialEffect = () => {
const isCurrentUserLoaded = useRecoilValueV2(isCurrentUserLoadedState);
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const store = useStore();
const [isInitialized, setIsInitialized] = useState(false);
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
const { updateDraft, applyChanges } = useMetadataStore();
useEffect(() => {
if (isInitialized) {
return;
}
if (isInitialized) return;
if (!isCurrentUserLoaded) return;
const shouldLoadReal = isWorkspaceActiveOrSuspended(currentWorkspace);
const loadObjectMetadata = async () => {
if (
isUndefinedOrNull(currentUser) ||
!isWorkspaceActiveOrSuspended(currentWorkspace)
) {
await loadMockedObjectMetadataItems();
} else {
if (shouldLoadReal) {
await refreshObjectMetadataItems();
} else {
await loadMockedObjectMetadataItems();
}
const loadedItems = store.get(objectMetadataItemsState.atom);
updateDraft('objects', loadedItems);
applyChanges();
setIsInitialized(true);
};
loadObjectMetadata();
}, [
currentUser,
currentWorkspace,
loadMockedObjectMetadataItems,
refreshObjectMetadataItems,
isInitialized,
isCurrentUserLoaded,
currentWorkspace,
refreshObjectMetadataItems,
loadMockedObjectMetadataItems,
store,
updateDraft,
applyChanges,
]);
return <></>;
return null;
};
@@ -1,5 +1,3 @@
import { useRecoilCallback } from 'recoil';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
import { currentUserState } from '@/auth/states/currentUserState';
@@ -8,6 +6,7 @@ import { currentWorkspaceDeletedMembersState } from '@/auth/states/currentWorksp
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { useInitializeFormatPreferences } from '@/localization/hooks/useInitializeFormatPreferences';
import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale.util';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
@@ -15,7 +14,8 @@ import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetReco
import { type ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { enUS } from 'date-fns/locale';
import { useStore } from 'jotai';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useRecoilCallback } from 'recoil';
import { useLocation } from 'react-router-dom';
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import { AppPath, type ObjectPermissions } from 'twenty-shared/types';
@@ -29,10 +29,12 @@ import { dateLocaleStateV2 } from '~/localization/states/dateLocaleStateV2';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
export const MetadataProviderEffect = () => {
export const UserMetadataProviderInitialEffect = () => {
const location = useLocation();
const isLoggedIn = useIsLogged();
const currentUser = useRecoilValueV2(currentUserState);
const store = useStore();
const [isInitialized, setIsInitialized] = useState(false);
const setCurrentUser = useSetRecoilStateV2(currentUserState);
const setCurrentWorkspace = useSetRecoilStateV2(currentWorkspaceState);
@@ -40,10 +42,22 @@ export const MetadataProviderEffect = () => {
currentUserWorkspaceState,
);
const setAvailableWorkspaces = useSetRecoilStateV2(availableWorkspacesState);
const { initializeFormatPreferences } = useInitializeFormatPreferences();
const isLoggedIn = useIsLogged();
const setCurrentWorkspaceMember = useSetRecoilStateV2(
currentWorkspaceMemberState,
);
const setCurrentWorkspaceMembers = useSetRecoilStateV2(
currentWorkspaceMembersState,
);
const setCurrentWorkspaceMembersWithDeleted = useSetRecoilStateV2(
currentWorkspaceDeletedMembersState,
);
const setIsCurrentUserLoaded = useSetRecoilStateV2(isCurrentUserLoadedState);
const store = useStore();
const { initializeFormatPreferences } = useInitializeFormatPreferences();
const isOnAuthPath =
isMatchingLocation(location, AppPath.Verify) ||
isMatchingLocation(location, AppPath.VerifyEmail);
const updateLocaleCatalog = useRecoilCallback(
({ snapshot, set }) =>
@@ -63,28 +77,23 @@ export const MetadataProviderEffect = () => {
[store],
);
const setCurrentWorkspaceMember = useSetRecoilStateV2(
currentWorkspaceMemberState,
);
const setCurrentWorkspaceMembers = useSetRecoilStateV2(
currentWorkspaceMembersState,
);
const setCurrentWorkspaceMembersWithDeleted = useSetRecoilStateV2(
currentWorkspaceDeletedMembersState,
);
const shouldSkip =
!isLoggedIn ||
isDefined(currentUser) ||
isMatchingLocation(location, AppPath.Verify) ||
isMatchingLocation(location, AppPath.VerifyEmail);
const shouldSkipUserQuery =
!isLoggedIn || isDefined(currentUser) || isOnAuthPath;
const { data: userQueryData, loading: userQueryLoading } =
useGetCurrentUserQuery({
skip: shouldSkip,
skip: shouldSkipUserQuery,
});
useEffect(() => {
if (isInitialized) return;
if (!isLoggedIn) {
setIsCurrentUserLoaded(true);
setIsInitialized(true);
return;
}
if (userQueryLoading || !isDefined(userQueryData?.currentUser)) {
return;
}
@@ -160,7 +169,12 @@ export const MetadataProviderEffect = () => {
if (isDefined(availableWorkspaces)) {
setAvailableWorkspaces(availableWorkspaces);
}
setIsCurrentUserLoaded(true);
setIsInitialized(true);
}, [
isInitialized,
isLoggedIn,
userQueryLoading,
userQueryData?.currentUser,
setCurrentUser,
@@ -172,6 +186,7 @@ export const MetadataProviderEffect = () => {
initializeFormatPreferences,
setCurrentWorkspaceMembersWithDeleted,
updateLocaleCatalog,
setIsCurrentUserLoaded,
]);
return null;
@@ -0,0 +1,87 @@
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useStore } from 'jotai';
import { useCallback, useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
import {
ViewType as CoreViewType,
useFindAllCoreViewsLazyQuery,
} from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
const INDEX_VIEW_TYPES = [
CoreViewType.TABLE,
CoreViewType.KANBAN,
CoreViewType.CALENDAR,
];
export const ViewMetadataProviderInitialEffect = () => {
const isLoggedIn = useIsLogged();
const isCurrentUserLoaded = useRecoilValueV2(isCurrentUserLoadedState);
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const store = useStore();
const [isInitialized, setIsInitialized] = useState(false);
const { updateDraft, applyChanges } = useMetadataStore();
const [findAllCoreViews] = useFindAllCoreViewsLazyQuery();
const setIndexCoreViews = useCallback(
(indexViews: CoreViewWithRelations[]) => {
const existingCoreViews = store.get(coreViewsState.atom);
const existingFieldsWidgetViews = existingCoreViews.filter(
(view) => view.type === CoreViewType.FIELDS_WIDGET,
);
const mergedViews = [...indexViews, ...existingFieldsWidgetViews];
if (!isDeeplyEqual(existingCoreViews, mergedViews)) {
store.set(coreViewsState.atom, mergedViews);
}
},
[store],
);
useEffect(() => {
if (isInitialized) return;
if (!isCurrentUserLoaded) return;
if (!isLoggedIn || !isWorkspaceActiveOrSuspended(currentWorkspace)) {
setIsInitialized(true);
return;
}
const loadViews = async () => {
const result = await findAllCoreViews({
variables: { viewTypes: INDEX_VIEW_TYPES },
fetchPolicy: 'network-only',
});
if (isDefined(result.data?.getCoreViews)) {
setIndexCoreViews(result.data.getCoreViews);
updateDraft('views', result.data.getCoreViews);
applyChanges();
}
setIsInitialized(true);
};
loadViews();
}, [
isInitialized,
isCurrentUserLoaded,
isLoggedIn,
currentWorkspace,
findAllCoreViews,
setIndexCoreViews,
updateDraft,
applyChanges,
]);
return null;
};
@@ -0,0 +1,111 @@
import {
ALL_METADATA_KEYS,
metadataStoreState,
type MetadataKey,
type MetadataLoadEntry,
} from '@/metadata-store/states/metadataStoreState';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { type createStore, useStore } from 'jotai';
import { useCallback } from 'react';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type JotaiStore = ReturnType<typeof createStore>;
const EMPTY_ENTRY: MetadataLoadEntry = {
current: [],
draft: [],
status: 'empty',
};
const areViewsConsistentWithObjects = (
viewsDraft: object[],
objectsCurrent: object[],
): boolean => {
const objectIds = new Set(
objectsCurrent.map((item) => (item as { id: string }).id),
);
return viewsDraft.every((view) =>
objectIds.has((view as { objectMetadataId: string }).objectMetadataId),
);
};
export const resetMetadataStore = (store: JotaiStore) => {
for (const key of ALL_METADATA_KEYS) {
store.set(metadataStoreState.atomFamily(key), EMPTY_ENTRY);
}
store.set(isAppMetadataReadyState.atom, false);
};
const promoteEntry = (store: JotaiStore, key: MetadataKey) => {
const entry = store.get(metadataStoreState.atomFamily(key));
store.set(metadataStoreState.atomFamily(key), {
current: entry.draft,
draft: [],
status: 'loaded',
});
};
export const useMetadataStore = () => {
const store = useStore();
const updateDraft = useCallback(
(key: MetadataKey, data: object[]) => {
const currentEntry = store.get(metadataStoreState.atomFamily(key));
if (
currentEntry.status === 'loaded' &&
isDeeplyEqual(currentEntry.current, data)
) {
return;
}
store.set(metadataStoreState.atomFamily(key), (prev) => ({
...prev,
draft: data,
status: 'draft_pending' as const,
}));
},
[store],
);
const applyChanges = useCallback((): boolean => {
let promoted = false;
for (const key of ALL_METADATA_KEYS) {
if (key === 'views') {
continue;
}
const entry = store.get(metadataStoreState.atomFamily(key));
if (entry.status === 'draft_pending') {
promoteEntry(store, key);
promoted = true;
}
}
const viewsEntry = store.get(metadataStoreState.atomFamily('views'));
if (viewsEntry.status === 'draft_pending') {
const objectsEntry = store.get(metadataStoreState.atomFamily('objects'));
if (
areViewsConsistentWithObjects(viewsEntry.draft, objectsEntry.current)
) {
promoteEntry(store, 'views');
promoted = true;
}
}
return promoted;
}, [store]);
const reset = useCallback(() => {
resetMetadataStore(store);
}, [store]);
return { updateDraft, applyChanges, resetMetadataStore: reset };
};
@@ -0,0 +1,50 @@
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { coreViewsState } from '@/views/states/coreViewState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
export const useReloadWorkspaceMetadata = () => {
const store = useStore();
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
const { updateDraft, applyChanges, resetMetadataStore } = useMetadataStore();
const reloadWorkspaceMetadata = useCallback(async () => {
resetMetadataStore();
await refreshObjectMetadataItems();
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objects', loadedObjects);
applyChanges();
const loadedViews = store.get(coreViewsState.atom);
updateDraft('views', loadedViews);
applyChanges();
}, [
resetMetadataStore,
refreshObjectMetadataItems,
store,
updateDraft,
applyChanges,
]);
const resetToMockedMetadata = useCallback(async () => {
resetMetadataStore();
await loadMockedObjectMetadataItems();
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objects', loadedObjects);
applyChanges();
}, [
resetMetadataStore,
loadMockedObjectMetadataItems,
store,
updateDraft,
applyChanges,
]);
return { reloadWorkspaceMetadata, resetToMockedMetadata };
};
@@ -1,6 +1,6 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const shouldAppBeLoadingState = createStateV2<boolean>({
key: 'shouldAppBeLoadingState',
export const isAppMetadataReadyState = createStateV2<boolean>({
key: 'isAppMetadataReadyState',
defaultValue: false,
});
@@ -8,6 +8,13 @@ export type MetadataKey =
| 'pageLayouts'
| 'logicFunctions';
export const ALL_METADATA_KEYS: MetadataKey[] = [
'objects',
'views',
'pageLayouts',
'logicFunctions',
];
export type MetadataLoadEntry = {
current: object[];
draft: object[];
@@ -1,30 +0,0 @@
import React from 'react';
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
export const ObjectMetadataItemsProvider = ({
children,
}: React.PropsWithChildren) => {
const objectMetadataItems = useRecoilValueV2(objectMetadataItemsState);
const shouldAppBeLoading = useRecoilValueV2(shouldAppBeLoadingState);
const shouldDisplayChildren =
!shouldAppBeLoading && objectMetadataItems.length > 0;
return (
<>
{shouldDisplayChildren ? (
<PreComputedChipGeneratorsProvider>
{children}
</PreComputedChipGeneratorsProvider>
) : (
<UserOrMetadataLoader />
)}
</>
);
};
@@ -8,18 +8,11 @@ import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-met
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
import { useFamilySelectorValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilySelectorValueV2';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useMemo } from 'react';
export const useColumnDefinitionsFromObjectMetadata = (
objectMetadataItem: ObjectMetadataItem,
) => {
const activeFieldMetadataItems = objectMetadataItem.readableFields.filter(
(field) =>
field.isActive &&
// Allow label identifier field (e.g. id for junction tables) even if it's a hidden system field
(!isHiddenSystemField(field) ||
field.id === objectMetadataItem.labelIdentifierFieldMetadataId),
);
const filterableFieldMetadataItems = useFamilySelectorValueV2(
availableFieldMetadataItemsForFilterFamilySelector,
{
@@ -34,10 +27,16 @@ export const useColumnDefinitionsFromObjectMetadata = (
},
);
const restrictedFieldMetadataIds: string[] = [];
const columnDefinitions: ColumnDefinition<FieldMetadata>[] = useMemo(() => {
const activeFieldMetadataItems =
objectMetadataItem.readableFields.filter(
(field) =>
field.isActive &&
(!isHiddenSystemField(field) ||
field.id === objectMetadataItem.labelIdentifierFieldMetadataId),
);
const columnDefinitions: ColumnDefinition<FieldMetadata>[] =
activeFieldMetadataItems
return activeFieldMetadataItems
.map((field, index) =>
formatFieldMetadataItemAsColumnDefinition({
position: index,
@@ -47,6 +46,7 @@ export const useColumnDefinitionsFromObjectMetadata = (
)
.filter(filterAvailableTableColumns)
.filter((column) => {
const restrictedFieldMetadataIds: string[] = [];
return !restrictedFieldMetadataIds.includes(column.fieldMetadataId);
})
.map((column) => {
@@ -65,6 +65,11 @@ export const useColumnDefinitionsFromObjectMetadata = (
isSortable: existsInSortDefinitions,
};
});
}, [
filterableFieldMetadataItems,
sortableFieldMetadataItems,
objectMetadataItem,
]);
return {
columnDefinitions,
@@ -1,5 +1,5 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
import {
@@ -39,7 +39,7 @@ const meta: Meta<typeof DashboardWidgetPlaceholder> = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
@@ -13,7 +13,7 @@ import { CatalogDecorator, type CatalogStory } from 'twenty-ui/testing';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@@ -284,7 +284,7 @@ export const WithNumberChart: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -372,7 +372,7 @@ export const WithGaugeChart: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -464,7 +464,7 @@ export const WithBarChart: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -559,7 +559,7 @@ export const SmallWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -658,7 +658,7 @@ export const MediumWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -757,7 +757,7 @@ export const LargeWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -852,7 +852,7 @@ export const WideWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -951,7 +951,7 @@ export const TallWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(widget);
snapshot.set(
pageLayoutPersistedComponentState.atomFamily({
@@ -1046,7 +1046,7 @@ export const WithManyToOneRelationFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
PageLayoutType.RECORD_PAGE,
@@ -1163,7 +1163,7 @@ export const WithOneToManyRelationFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
PageLayoutType.RECORD_PAGE,
@@ -1272,7 +1272,7 @@ export const OneToManyRelationFieldWidgetWithSeeAllButton: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
PageLayoutType.RECORD_PAGE,
@@ -1408,7 +1408,7 @@ export const OnMobile: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
PageLayoutType.RECORD_PAGE,
@@ -1507,7 +1507,7 @@ export const InSidePanel: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
PageLayoutType.RECORD_PAGE,
@@ -1667,7 +1667,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetRenderer> = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
if (state === 'hover') {
snapshot.set(
@@ -10,7 +10,7 @@ import { expect, userEvent, waitFor, within } from 'storybook/test';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilyStateV2 } from '@/object-record/record-store/states/recordStoreFamilyStateV2';
@@ -369,7 +369,7 @@ export const TextFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -467,7 +467,7 @@ export const AddressFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -568,7 +568,7 @@ export const NumberFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -666,7 +666,7 @@ export const LinkFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -764,7 +764,7 @@ export const ManyToOneRelationFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -873,7 +873,7 @@ export const OneToManyRelationFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -973,7 +973,7 @@ export const BooleanFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -1070,7 +1070,7 @@ export const CurrencyFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -1167,7 +1167,7 @@ export const EmailsFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
personObjectMetadataItem.id,
@@ -1265,7 +1265,7 @@ export const PhonesFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
personObjectMetadataItem.id,
@@ -1363,7 +1363,7 @@ export const SelectFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
opportunityObjectMetadataItem.id,
@@ -1466,7 +1466,7 @@ export const MultiSelectFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -1568,7 +1568,7 @@ export const TimelineActivityRelationFieldWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
timelineActivityObjectMetadataItem.id,
@@ -1676,7 +1676,7 @@ export const ManyToOneRelationCardWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -1793,7 +1793,7 @@ export const OneToManyRelationCardWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -1892,7 +1892,7 @@ export const TimelineActivityRelationCardWidget: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
timelineActivityObjectMetadataItem.id,
@@ -2062,7 +2062,7 @@ export const OneToManyRelationCardWidgetWithProgressiveLoading: Story = {
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(shouldAppBeLoadingState.atom, false);
jotaiStore.set(isAppMetadataReadyState.atom, true);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
@@ -1,20 +1,16 @@
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { useAuth } from '@/auth/hooks/useAuth';
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
export const useImpersonationAuth = () => {
const { getAuthTokensFromLoginToken } = useAuth();
const setShouldAppBeLoading = useSetRecoilStateV2(shouldAppBeLoadingState);
const setIsAppEffectRedirectEnabled = useSetRecoilStateV2(
isAppEffectRedirectEnabledState,
);
const executeImpersonationAuth = async (loginToken: string) => {
setShouldAppBeLoading(true);
setIsAppEffectRedirectEnabled(false);
await getAuthTokensFromLoginToken(loginToken);
setShouldAppBeLoading(false);
setIsAppEffectRedirectEnabled(true);
};
@@ -207,7 +207,6 @@ Core metadata states used by record-table, record-board, views, etc.
```
object-metadata/states/lastFieldMetadataItemUpdateState.ts
object-metadata/states/objectMetadataItemsState.ts
object-metadata/states/shouldAppBeLoadingState.ts
```
### Selectors (12 files)
@@ -45,6 +45,7 @@ export const createStateV2 = <ValueType>({
useCookieStorage.cookieKey,
defaultValue,
storage,
{ getOnInit: true },
) as StateAtom<ValueType>;
} else if (useLocalStorage) {
baseAtom = atomWithStorage<ValueType>(
@@ -1,121 +0,0 @@
import { useMetadataStore } from '@/app/hooks/useMetadataStore';
import { metadataStoreState } from '@/app/states/metadataStoreState';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useFamilyRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilyRecoilValueV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useStore } from 'jotai';
import { useCallback, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
import {
ViewType as CoreViewType,
useFindAllCoreViewsQuery,
} from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
const INDEX_VIEW_TYPES = [
CoreViewType.TABLE,
CoreViewType.KANBAN,
CoreViewType.CALENDAR,
];
export const EagerMetadataLoadEffect = () => {
const location = useLocation();
const isLoggedIn = useIsLogged();
const currentUser = useRecoilValueV2(currentUserState);
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const store = useStore();
const objectsEntry = useFamilyRecoilValueV2(metadataStoreState, 'objects');
const viewsEntry = useFamilyRecoilValueV2(metadataStoreState, 'views');
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
const { updateDraft, applyChanges } = useMetadataStore();
const isOnAuthPath =
isMatchingLocation(location, AppPath.Verify) ||
isMatchingLocation(location, AppPath.VerifyEmail);
const { data: queryDataCoreViews } = useFindAllCoreViewsQuery({
skip: !isLoggedIn || viewsEntry.status !== 'empty' || isOnAuthPath,
variables: { viewTypes: INDEX_VIEW_TYPES },
});
const setIndexCoreViews = useCallback(
(indexViews: CoreViewWithRelations[]) => {
const existingCoreViews = store.get(coreViewsState.atom);
const existingFieldsWidgetViews = existingCoreViews.filter(
(view) => view.type === CoreViewType.FIELDS_WIDGET,
);
const mergedViews = [...indexViews, ...existingFieldsWidgetViews];
if (!isDeeplyEqual(existingCoreViews, mergedViews)) {
store.set(coreViewsState.atom, mergedViews);
}
},
[store],
);
useEffect(() => {
if (objectsEntry.status !== 'empty') {
return;
}
if (isLoggedIn && !isDefined(currentUser)) {
return;
}
const loadObjectMetadata = async () => {
if (!isLoggedIn || !isWorkspaceActiveOrSuspended(currentWorkspace)) {
await loadMockedObjectMetadataItems();
} else {
await refreshObjectMetadataItems();
}
const loadedItems = store.get(objectMetadataItemsState.atom);
updateDraft('objects', loadedItems);
};
loadObjectMetadata();
}, [
currentUser,
currentWorkspace,
isLoggedIn,
loadMockedObjectMetadataItems,
objectsEntry.status,
refreshObjectMetadataItems,
store,
updateDraft,
]);
useEffect(() => {
if (!isDefined(queryDataCoreViews?.getCoreViews)) {
return;
}
setIndexCoreViews(queryDataCoreViews.getCoreViews);
updateDraft('views', queryDataCoreViews.getCoreViews);
}, [queryDataCoreViews?.getCoreViews, setIndexCoreViews, updateDraft]);
useEffect(() => {
if (
objectsEntry.status === 'draft_pending' &&
viewsEntry.status === 'draft_pending'
) {
applyChanges();
}
}, [objectsEntry.status, viewsEntry.status, applyChanges]);
return null;
};
@@ -1,4 +1,4 @@
import { metadataStoreState } from '@/app/states/metadataStoreState';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { recordPageLayoutsState } from '@/page-layout/states/recordPageLayoutsState';
import { type PageLayout } from '@/page-layout/types/PageLayout';
@@ -8,8 +8,8 @@ import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useStore } from 'jotai';
import { useCallback, useEffect } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useLocation } from 'react-router-dom';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -29,6 +29,7 @@ export const LazyMetadataLoadEffect = () => {
const store = useStore();
const setLogicFunctions = useSetRecoilState(logicFunctionsState);
const { updateDraft, applyChanges } = useMetadataStore();
const isOnAuthPath =
isMatchingLocation(location, AppPath.Verify) ||
@@ -75,14 +76,8 @@ export const LazyMetadataLoadEffect = () => {
if (!isDeeplyEqual(existingRecordPageLayouts, recordPageLayouts)) {
set(recordPageLayoutsState, recordPageLayouts);
}
store.set(metadataStoreState.atomFamily('pageLayouts'), {
current: recordPageLayouts,
draft: [],
status: 'loaded',
});
},
[store],
[],
);
useEffect(() => {
@@ -102,7 +97,14 @@ export const LazyMetadataLoadEffect = () => {
queryDataRecordPageLayouts.getPageLayouts.map(transformPageLayout);
setRecordPageLayouts(transformedPageLayouts);
}, [queryDataRecordPageLayouts?.getPageLayouts, setRecordPageLayouts]);
updateDraft('pageLayouts', transformedPageLayouts);
applyChanges();
}, [
queryDataRecordPageLayouts?.getPageLayouts,
setRecordPageLayouts,
updateDraft,
applyChanges,
]);
useEffect(() => {
if (!isDefined(logicFunctionsData?.findManyLogicFunctions)) {
@@ -110,13 +112,14 @@ export const LazyMetadataLoadEffect = () => {
}
setLogicFunctions(logicFunctionsData.findManyLogicFunctions);
store.set(metadataStoreState.atomFamily('logicFunctions'), {
current: logicFunctionsData.findManyLogicFunctions,
draft: [],
status: 'loaded',
});
}, [logicFunctionsData?.findManyLogicFunctions, setLogicFunctions, store]);
updateDraft('logicFunctions', logicFunctionsData.findManyLogicFunctions);
applyChanges();
}, [
logicFunctionsData?.findManyLogicFunctions,
setLogicFunctions,
updateDraft,
applyChanges,
]);
return null;
};