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,45 +0,0 @@
import { currentUserState } from '@/auth/states/currentUserState';
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 { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const ObjectMetadataItemsLoadEffect = () => {
const currentUser = useRecoilValueV2(currentUserState);
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const [isInitialized, setIsInitialized] = useState(false);
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
useEffect(() => {
if (isInitialized) {
return;
}
const loadObjectMetadata = async () => {
if (
isUndefinedOrNull(currentUser) ||
!isWorkspaceActiveOrSuspended(currentWorkspace)
) {
await loadMockedObjectMetadataItems();
} else {
await refreshObjectMetadataItems();
}
setIsInitialized(true);
};
loadObjectMetadata();
}, [
currentUser,
currentWorkspace,
loadMockedObjectMetadataItems,
refreshObjectMetadataItems,
isInitialized,
]);
return <></>;
};
@@ -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,6 +0,0 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const shouldAppBeLoadingState = createStateV2<boolean>({
key: 'shouldAppBeLoadingState',
defaultValue: false,
});