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,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;
};
@@ -1,178 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceDeletedMembersState } from '@/auth/states/currentWorkspaceDeletedMembersState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useInitializeFormatPreferences } from '@/localization/hooks/useInitializeFormatPreferences';
import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale.util';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { type ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { enUS } from 'date-fns/locale';
import { useStore } from 'jotai';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import { AppPath, type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
useGetCurrentUserQuery,
type WorkspaceMember,
} from '~/generated-metadata/graphql';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { dateLocaleStateV2 } from '~/localization/states/dateLocaleStateV2';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
export const MetadataProviderEffect = () => {
const location = useLocation();
const currentUser = useRecoilValueV2(currentUserState);
const setCurrentUser = useSetRecoilStateV2(currentUserState);
const setCurrentWorkspace = useSetRecoilStateV2(currentWorkspaceState);
const setCurrentUserWorkspace = useSetRecoilStateV2(
currentUserWorkspaceState,
);
const setAvailableWorkspaces = useSetRecoilStateV2(availableWorkspacesState);
const { initializeFormatPreferences } = useInitializeFormatPreferences();
const isLoggedIn = useIsLogged();
const store = useStore();
const updateLocaleCatalog = useRecoilCallback(
({ snapshot, set }) =>
async (newLocale: keyof typeof APP_LOCALES) => {
const localeValue = snapshot.getLoadable(dateLocaleState).getValue();
if (localeValue.locale !== newLocale) {
getDateFnsLocale(newLocale).then((localeCatalog) => {
const newValue = {
locale: newLocale,
localeCatalog: localeCatalog || enUS,
};
set(dateLocaleState, newValue);
store.set(dateLocaleStateV2.atom, newValue);
});
}
},
[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 { data: userQueryData, loading: userQueryLoading } =
useGetCurrentUserQuery({
skip: shouldSkip,
});
useEffect(() => {
if (userQueryLoading || !isDefined(userQueryData?.currentUser)) {
return;
}
setCurrentUser(userQueryData.currentUser);
if (isDefined(userQueryData.currentUser.currentWorkspace)) {
setCurrentWorkspace({
...userQueryData.currentUser.currentWorkspace,
defaultRole:
userQueryData.currentUser.currentWorkspace.defaultRole ?? null,
workspaceCustomApplication:
userQueryData.currentUser.currentWorkspace
.workspaceCustomApplication ?? null,
});
}
if (isDefined(userQueryData.currentUser.currentUserWorkspace)) {
setCurrentUserWorkspace({
permissionFlags:
userQueryData.currentUser.currentUserWorkspace.permissionFlags ?? [],
twoFactorAuthenticationMethodSummary:
userQueryData.currentUser.currentUserWorkspace
.twoFactorAuthenticationMethodSummary ?? [],
objectsPermissions:
(userQueryData.currentUser.currentUserWorkspace
.objectsPermissions as Array<
ObjectPermissions & { objectMetadataId: string }
>) ?? [],
});
}
const {
workspaceMember,
workspaceMembers,
deletedWorkspaceMembers,
availableWorkspaces,
} = userQueryData.currentUser;
const affectDefaultValuesOnEmptyWorkspaceMemberFields = (
workspaceMember: WorkspaceMember,
) => {
return {
...workspaceMember,
colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'System',
locale:
(workspaceMember.locale as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE,
};
};
if (isDefined(workspaceMember)) {
const updatedWorkspaceMember =
affectDefaultValuesOnEmptyWorkspaceMemberFields(workspaceMember);
setCurrentWorkspaceMember(updatedWorkspaceMember);
updateLocaleCatalog(updatedWorkspaceMember.locale);
initializeFormatPreferences(updatedWorkspaceMember);
dynamicActivate(
(workspaceMember.locale as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE,
);
}
if (isDefined(workspaceMembers)) {
setCurrentWorkspaceMembers(workspaceMembers);
}
if (isDefined(deletedWorkspaceMembers)) {
setCurrentWorkspaceMembersWithDeleted(deletedWorkspaceMembers);
}
if (isDefined(availableWorkspaces)) {
setAvailableWorkspaces(availableWorkspaces);
}
}, [
userQueryLoading,
userQueryData?.currentUser,
setCurrentUser,
setCurrentUserWorkspace,
setCurrentWorkspaceMembers,
setAvailableWorkspaces,
setCurrentWorkspace,
setCurrentWorkspaceMember,
initializeFormatPreferences,
setCurrentWorkspaceMembersWithDeleted,
updateLocaleCatalog,
]);
return null;
};
@@ -1,40 +0,0 @@
import React from 'react';
import { isAppLoadingState } from '@/app/states/isAppLoadingState';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { UserContext } from '@/users/contexts/UserContext';
import { useLocation } from 'react-router-dom';
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);
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);
return shouldShowLoader ? (
<UserOrMetadataLoader />
) : (
<UserContext.Provider
value={{
dateFormat,
timeFormat,
timeZone,
}}
>
{children}
</UserContext.Provider>
);
};