feat: migrate objectMetadata reads to granular metadata store (#18643)

## Summary

Consolidates `objectMetadataItems` onto the metadata store as the
**single source of truth**, replacing the previous dual-store approach
(separate `objectMetadataItemsState` atom + untyped
`metadataStoreState`).

### Architecture: three-layer design

```
┌─────────────────────────────────────────────────────────┐
│ Store Layer (granular, typed)                           │
│  objectMetadataItems → FlatObjectMetadataItem[]         │
│  fieldMetadataItems  → FlatFieldMetadataItem[]          │
│  indexMetadataItems  → FlatIndexMetadataItem[]           │
└────────────────┬────────────────────────────────────────┘
                 │ .current (never draft)
┌────────────────▼────────────────────────────────────────┐
│ Selectors (typed read-only)                             │
│  objectMetadataItemsSelector                            │
│  fieldMetadataItemsSelector                             │
│  indexMetadataItemsSelector                             │
│  metadataStoreStatusFamilySelector                      │
│  isSystemObjectByNameSingularFamilySelector (narrow)    │
│  activeObjectNameSingularsSelector (narrow)             │
└────────────────┬────────────────────────────────────────┘
                 │ joins objects + fields + indexes + permissions
┌────────────────▼────────────────────────────────────────┐
│ Joining Selector                                        │
│  objectMetadataItemsWithFieldsSelector                  │
│  → produces full ObjectMetadataItem[] with              │
│    readableFields / updatableFields from permissions    │
│  → 12 existing selectors repointed here                 │
└─────────────────────────────────────────────────────────┘
```

### Key changes

- **Granular flat types** (`FlatObjectMetadataItem`,
`FlatFieldMetadataItem`, `FlatIndexMetadataItem`) — objects stored
without embedded fields/indexes, matching backend "Flat" naming
convention
- **Typed write API** — `updateDraft` is now generic via
`MetadataEntityTypeMap`, giving compile-time safety on what data shape
goes to each key
- **Write path refactored** — fetch → split into flat entities via
`splitObjectMetadataItemWithRelated` → write to metadata store directly.
No more dual-write through `objectMetadataItemsState`. Permissions
enrichment moved from write path into the joining selector.
- **SSE effects write directly** — `ObjectMetadataItemSSEEffect` and
`FieldMetadataSSEEffect` now patch the store from the SSE event payload
(create/update/delete) instead of triggering a full re-fetch
- **`objectMetadataItemsState` bridge** — converted from writable
`createAtomState` to read-only `createAtomSelector` that delegates to
the joining selector. All 100+ existing consumers continue to work
without code changes.
- **All selectors use Twenty state API** — `createAtomSelector` /
`createAtomFamilySelector` throughout, no raw `atom()`
- **Narrow selectors** for hot paths —
`isSystemObjectByNameSingularFamilySelector` and
`activeObjectNameSingularsSelector` read from flat objects only,
avoiding re-renders when fields/indexes/permissions change. Placed in
`object-metadata/states/` as higher-level business selectors.
- **Test helper** — `setTestObjectMetadataItemsInMetadataStore` for
tests that need to set up composite object metadata through the store
(clearly named as a testing utility)

### Naming conventions

- `ObjectMetadataItemWithRelated` — type for objects with embedded
fields/indexes (input to split utility)
- `FlatObjectMetadataItem` / `FlatFieldMetadataItem` /
`FlatIndexMetadataItem` — granular store types
- Selector names don't expose "Current" — that's an internal detail of
the metadata store API

### Future work

- Optimistic update API (`updateCurrentOptimistically` with rollback)
- Migrate remaining entities (views, pageLayouts, etc.) to the same
pattern
- Gradually remove `objectMetadataItemsState` bridge once all direct
imports are replaced

## Test plan

- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx lint:diff-with-main twenty-front` passes
- [ ] Verify app loads correctly with metadata from the store
- [ ] Verify SSE updates (object/field changes) propagate correctly
- [ ] Run existing test suites to confirm no regressions
This commit is contained in:
Charles Bochet 2026-03-14 12:54:19 +01:00 committed by GitHub
parent 48172d60fd
commit 40ff109179
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 657 additions and 315 deletions

View file

@ -6,8 +6,8 @@ import { Provider as JotaiProvider } from 'jotai';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
@ -140,8 +140,8 @@ describe('useActivityTargetObjectRecords', () => {
}),
);
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);

View file

@ -1,5 +1,5 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/metadata-store/states/objectMetadataItemsSelector';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
export const objectPermissionsFamilySelector = createAtomFamilySelector<
@ -14,7 +14,7 @@ export const objectPermissionsFamilySelector = createAtomFamilySelector<
({ objectNameSingular }) =>
({ get }) => {
const currentUserWorkspace = get(currentUserWorkspaceState);
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsSelector);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === objectNameSingular,

View file

@ -3,7 +3,7 @@ import {
type Meta,
type StoryObj,
} from '@storybook/react-vite';
import { expect, userEvent, within } from 'storybook/test';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
@ -152,8 +152,10 @@ export const LimitedPermissions: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('Go to People')).toBeVisible();
expect(canvas.queryByText('Go to Opportunities')).not.toBeInTheDocument();
expect(canvas.queryByText('Go to Tasks')).not.toBeInTheDocument();
await waitFor(() => {
expect(canvas.queryByText('Go to Opportunities')).not.toBeInTheDocument();
expect(canvas.queryByText('Go to Tasks')).not.toBeInTheDocument();
});
expect(await canvas.findByText('Go to Settings')).toBeVisible();
expect(await canvas.findByText('Go to Notes')).toBeVisible();
},

View file

@ -1,7 +1,6 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
import { AllMetadataName } from '~/generated-metadata/graphql';
@ -11,9 +10,6 @@ export const FieldMetadataSSEEffect = () => {
const store = useStore();
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { updateDraft, applyChanges } = useMetadataStore();
useListenToEventsForQuery({
queryId,
operationSignature: {
@ -24,12 +20,52 @@ export const FieldMetadataSSEEffect = () => {
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.fieldMetadata,
onMetadataOperationBrowserEvent: async () => {
await refreshObjectMetadataItems();
onMetadataOperationBrowserEvent: (eventDetail) => {
const entry = store.get(
metadataStoreState.atomFamily('fieldMetadataItems'),
);
const currentFields = entry.current as FlatFieldMetadataItem[];
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objectMetadataItems', loadedObjects);
applyChanges();
switch (eventDetail.operation.type) {
case 'create': {
const createdField = eventDetail.operation
.createdRecord as unknown as FlatFieldMetadataItem;
store.set(metadataStoreState.atomFamily('fieldMetadataItems'), {
...entry,
current: [...currentFields, createdField],
});
break;
}
case 'update': {
const updatedField = eventDetail.operation
.updatedRecord as unknown as FlatFieldMetadataItem;
store.set(metadataStoreState.atomFamily('fieldMetadataItems'), {
...entry,
current: currentFields.map((field) =>
field.id === updatedField.id
? { ...field, ...updatedField }
: field,
),
});
break;
}
case 'delete': {
const deletedFieldId = eventDetail.operation
.deletedRecordId as string;
store.set(metadataStoreState.atomFamily('fieldMetadataItems'), {
...entry,
current: currentFields.filter(
(field) => field.id !== deletedFieldId,
),
});
break;
}
default:
return;
}
},
});

View file

@ -1,7 +1,6 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
@ -18,9 +17,6 @@ export const ObjectMetadataItemSSEEffect = () => {
const store = useStore();
const client = useApolloClient();
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { updateDraft, applyChanges } = useMetadataStore();
useListenToEventsForQuery({
queryId,
operationSignature: {
@ -31,12 +27,52 @@ export const ObjectMetadataItemSSEEffect = () => {
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.objectMetadata,
onMetadataOperationBrowserEvent: async () => {
await refreshObjectMetadataItems();
onMetadataOperationBrowserEvent: async (eventDetail) => {
const entry = store.get(
metadataStoreState.atomFamily('objectMetadataItems'),
);
const currentObjects = entry.current as FlatObjectMetadataItem[];
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objectMetadataItems', loadedObjects);
applyChanges();
switch (eventDetail.operation.type) {
case 'create': {
const createdObject = eventDetail.operation
.createdRecord as unknown as FlatObjectMetadataItem;
store.set(metadataStoreState.atomFamily('objectMetadataItems'), {
...entry,
current: [...currentObjects, createdObject],
});
break;
}
case 'update': {
const updatedObject = eventDetail.operation
.updatedRecord as unknown as FlatObjectMetadataItem;
store.set(metadataStoreState.atomFamily('objectMetadataItems'), {
...entry,
current: currentObjects.map((object) =>
object.id === updatedObject.id
? { ...object, ...updatedObject }
: object,
),
});
break;
}
case 'delete': {
const deletedObjectId = eventDetail.operation
.deletedRecordId as string;
store.set(metadataStoreState.atomFamily('objectMetadataItems'), {
...entry,
current: currentObjects.filter(
(object) => object.id !== deletedObjectId,
),
});
break;
}
default:
return;
}
const navigationMenuItemsResult = await client.query({
query: FindManyNavigationMenuItemsDocument,

View file

@ -1,23 +1,18 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
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 { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useStore } from 'jotai';
import { useEffect, useState } from 'react';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
export const ObjectMetadataProviderInitialEffect = () => {
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const store = useStore();
const [isInitialized, setIsInitialized] = useState(false);
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
const { updateDraft, applyChanges } = useMetadataStore();
useEffect(() => {
if (isInitialized) {
@ -36,9 +31,6 @@ export const ObjectMetadataProviderInitialEffect = () => {
await loadMockedObjectMetadataItems();
}
const loadedItems = store.get(objectMetadataItemsState.atom);
updateDraft('objectMetadataItems', loadedItems);
applyChanges();
setIsInitialized(true);
};
@ -49,9 +41,6 @@ export const ObjectMetadataProviderInitialEffect = () => {
currentWorkspace,
refreshObjectMetadataItems,
loadMockedObjectMetadataItems,
store,
updateDraft,
applyChanges,
]);
return null;

View file

@ -1,5 +1,6 @@
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useSetIndexViews } from '@/metadata-store/hooks/useSetIndexViews';
import { type View } from '@/views/types/View';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useApolloClient } from '@apollo/client/react';
@ -24,7 +25,8 @@ export const useFetchAndLoadIndexViews = () => {
if (isDefined(result.data?.getCoreViews)) {
setIndexViews(result.data.getCoreViews);
updateDraft('views', result.data.getCoreViews);
// TODO: align generated ViewType with app ViewType to remove this cast
updateDraft('views', result.data.getCoreViews as unknown as View[]);
applyChanges();
}
}, [client, setIndexViews, updateDraft, applyChanges]);

View file

@ -5,6 +5,7 @@ import {
type MetadataEntityKey,
type MetadataStoreItem,
} from '@/metadata-store/states/metadataStoreState';
import { type MetadataEntityTypeMap } from '@/metadata-store/types/MetadataEntityTypeMap';
import { useStore, type createStore } from 'jotai';
import { useCallback } from 'react';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@ -55,7 +56,7 @@ export const useMetadataStore = () => {
const store = useStore();
const updateDraft = useCallback(
(key: MetadataEntityKey, data: object[]) => {
<K extends MetadataEntityKey>(key: K, data: MetadataEntityTypeMap[K][]) => {
const currentEntry = store.get(metadataStoreState.atomFamily(key));
if (

View file

@ -1,7 +1,7 @@
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 { type View } from '@/views/types/View';
import { coreViewsState } from '@/views/states/coreViewState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
@ -16,11 +16,9 @@ export const useReloadWorkspaceMetadata = () => {
resetMetadataStore();
await refreshObjectMetadataItems();
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objectMetadataItems', loadedObjects);
applyChanges();
const loadedViews = store.get(coreViewsState.atom);
// TODO: align generated ViewType with app ViewType to remove this cast
const loadedViews = store.get(coreViewsState.atom) as unknown as View[];
updateDraft('views', loadedViews);
applyChanges();
}, [
@ -35,16 +33,7 @@ export const useReloadWorkspaceMetadata = () => {
resetMetadataStore();
await loadMockedObjectMetadataItems();
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objectMetadataItems', loadedObjects);
applyChanges();
}, [
resetMetadataStore,
loadMockedObjectMetadataItems,
store,
updateDraft,
applyChanges,
]);
}, [resetMetadataStore, loadMockedObjectMetadataItems]);
return { reloadWorkspaceMetadata, resetToMockedMetadata };
};

View file

@ -0,0 +1,14 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const fieldMetadataItemsSelector = createAtomSelector<
FlatFieldMetadataItem[]
>({
key: 'fieldMetadataItemsSelector',
get: ({ get }) => {
const storeItem = get(metadataStoreState, 'fieldMetadataItems');
return storeItem.current as FlatFieldMetadataItem[];
},
});

View file

@ -0,0 +1,14 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatIndexMetadataItem } from '@/metadata-store/types/FlatIndexMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const indexMetadataItemsSelector = createAtomSelector<
FlatIndexMetadataItem[]
>({
key: 'indexMetadataItemsSelector',
get: ({ get }) => {
const storeItem = get(metadataStoreState, 'indexMetadataItems');
return storeItem.current as FlatIndexMetadataItem[];
},
});

View file

@ -8,6 +8,7 @@ export type MetadataEntityStoreStatus =
export const ALL_METADATA_ENTITY_KEYS = [
'objectMetadataItems',
'fieldMetadataItems',
'indexMetadataItems',
'views',
'viewFields',
'viewFilters',

View file

@ -0,0 +1,20 @@
import {
metadataStoreState,
type MetadataEntityKey,
type MetadataEntityStoreStatus,
} from '@/metadata-store/states/metadataStoreState';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
export const metadataStoreStatusFamilySelector = createAtomFamilySelector<
MetadataEntityStoreStatus,
MetadataEntityKey
>({
key: 'metadataStoreStatusFamilySelector',
get:
(entityKey: MetadataEntityKey) =>
({ get }) => {
const storeItem = get(metadataStoreState, entityKey);
return storeItem.status;
},
});

View file

@ -0,0 +1,14 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const objectMetadataItemsSelector = createAtomSelector<
FlatObjectMetadataItem[]
>({
key: 'objectMetadataItemsSelector',
get: ({ get }) => {
const storeItem = get(metadataStoreState, 'objectMetadataItems');
return storeItem.current as FlatObjectMetadataItem[];
},
});

View file

@ -0,0 +1,5 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
export type FlatFieldMetadataItem = FieldMetadataItem & {
objectMetadataId: string;
};

View file

@ -0,0 +1,5 @@
import { type IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
export type FlatIndexMetadataItem = IndexMetadataItem & {
objectMetadataId: string;
};

View file

@ -0,0 +1,6 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export type FlatObjectMetadataItem = Omit<
ObjectMetadataItem,
'fields' | 'readableFields' | 'updatableFields' | 'indexMetadatas'
>;

View file

@ -0,0 +1,23 @@
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 PageLayout } from '@/page-layout/types/PageLayout';
import { type LogicFunction } from '@/settings/logic-functions/states/logicFunctionsState';
import { type View } from '@/views/types/View';
import { type ViewField } from '@/views/types/ViewField';
import { type ViewFilter } from '@/views/types/ViewFilter';
import { type ViewSort } from '@/views/types/ViewSort';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export type MetadataEntityTypeMap = {
objectMetadataItems: FlatObjectMetadataItem;
fieldMetadataItems: FlatFieldMetadataItem;
indexMetadataItems: FlatIndexMetadataItem;
views: View;
viewFields: ViewField;
viewFilters: ViewFilter;
viewSorts: ViewSort;
pageLayouts: PageLayout;
logicFunctions: LogicFunction;
navigationMenuItems: NavigationMenuItem;
};

View file

@ -0,0 +1,49 @@
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 ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export type ObjectMetadataItemWithRelated = Omit<
ObjectMetadataItem,
'readableFields' | 'updatableFields'
>;
type SplitResult = {
flatObjects: FlatObjectMetadataItem[];
flatFields: FlatFieldMetadataItem[];
flatIndexes: FlatIndexMetadataItem[];
};
export const splitObjectMetadataItemWithRelated = (
objectMetadataItemsWithRelated: ObjectMetadataItemWithRelated[],
): SplitResult => {
const flatObjects: FlatObjectMetadataItem[] = [];
const flatFields: FlatFieldMetadataItem[] = [];
const flatIndexes: FlatIndexMetadataItem[] = [];
for (const objectMetadataItemWithRelated of objectMetadataItemsWithRelated) {
const {
fields = [],
indexMetadatas = [],
...objectProperties
} = objectMetadataItemWithRelated;
flatObjects.push(objectProperties);
for (const field of fields) {
flatFields.push({
...field,
objectMetadataId: objectMetadataItemWithRelated.id,
});
}
for (const index of indexMetadatas) {
flatIndexes.push({
...index,
objectMetadataId: objectMetadataItemWithRelated.id,
});
}
}
return { flatObjects, flatFields, flatIndexes };
};

View file

@ -4,8 +4,8 @@ import { Provider as JotaiProvider } from 'jotai';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { coreViewsState } from '@/views/states/coreViewState';
@ -29,8 +29,8 @@ const renderHooks = ({
withCurrentUser: boolean;
withExistingView: boolean;
}) => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);

View file

@ -8,8 +8,8 @@ import {
variables,
} from '@/object-metadata/hooks/__mocks__/useFilteredObjectMetadataItems';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { isDefined } from 'twenty-shared/utils';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
@ -28,8 +28,8 @@ const mocks = [
];
const Wrapper = ({ children }: { children: ReactNode }) => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);

View file

@ -2,8 +2,8 @@ import { renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
@ -12,8 +12,8 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
describe('useGetObjectRecordIdentifierByNameSingular', () => {
beforeEach(() => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
});

View file

@ -4,8 +4,8 @@ import { type ReactNode } from 'react';
import { Provider as JotaiProvider } from 'jotai';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
const Wrapper = ({ children }: { children: ReactNode }) => (
@ -16,8 +16,8 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
describe('useGetRelationMetadata', () => {
beforeEach(() => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
});
@ -50,11 +50,11 @@ describe('useGetRelationMetadata', () => {
(field) => field.name === 'pointOfContact',
);
expect(relationObjectMetadataItem).toEqual(
expectedRelationObjectMetadataItem,
expect(relationObjectMetadataItem).toMatchObject(
expectedRelationObjectMetadataItem!,
);
expect(relationFieldMetadataItem).toEqual(
expectedRelationFieldMetadataItem,
expect(relationFieldMetadataItem).toMatchObject(
expectedRelationFieldMetadataItem!,
);
expect(relationType).toBe('ONE_TO_MANY');
});

View file

@ -1,9 +1,12 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { useMemo } from 'react';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const useFilteredObjectMetadataItems = () => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItemsWithFields = useAtomStateValue(
objectMetadataItemsWithFieldsSelector,
);
const objectMetadataItems = objectMetadataItemsWithFields;
const activeNonSystemObjectMetadataItems = useMemo(
() =>

View file

@ -1,32 +1,30 @@
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useCallback } from 'react';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
import { useStore } from 'jotai';
import { useCallback } from 'react';
export const useLoadMockedObjectMetadataItems = () => {
const store = useStore();
const { updateDraft, applyChanges } = useMetadataStore();
const loadMockedObjectMetadataItems = useCallback(async () => {
const { generatedMockObjectMetadataItems } = await import(
'~/testing/utils/generatedMockObjectMetadataItems'
);
if (
!isDeeplyEqual(
store.get(objectMetadataItemsState.atom),
generatedMockObjectMetadataItems,
)
) {
store.set(
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
}
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems);
updateDraft('objectMetadataItems', flatObjects);
updateDraft('fieldMetadataItems', flatFields);
updateDraft('indexMetadataItems', flatIndexes);
applyChanges();
if (store.get(isAppEffectRedirectEnabledState.atom) === false) {
store.set(isAppEffectRedirectEnabledState.atom, true);
}
}, [store]);
}, [store, updateDraft, applyChanges]);
return {
loadMockedObjectMetadataItems,

View file

@ -1,6 +1,6 @@
import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@ -18,7 +18,10 @@ export const useObjectMetadataItem = ({
},
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItemsWithFields = useAtomStateValue(
objectMetadataItemsWithFieldsSelector,
);
const objectMetadataItems = objectMetadataItemsWithFields;
if (!isDefined(objectMetadataItem)) {
throw new ObjectMetadataItemNotFoundError(

View file

@ -1,9 +1,11 @@
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
export const useObjectMetadataItems = () => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItemsWithFields = useAtomStateValue(
objectMetadataItemsWithFieldsSelector,
);
const objectMetadataItems = objectMetadataItemsWithFields;
return {
objectMetadataItems,

View file

@ -1,75 +1,22 @@
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { enrichObjectMetadataItemsWithPermissions } from '@/object-metadata/utils/enrichObjectMetadataItemsWithPermissions';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
import { type FetchPolicy } from '@apollo/client';
import { useApolloClient } from '@apollo/client/react';
import { useCallback } from 'react';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { type ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
export const useRefreshObjectMetadataItems = (
fetchPolicy: FetchPolicy = 'network-only',
) => {
const store = useStore();
const client = useApolloClient();
const { updateDraft, applyChanges } = useMetadataStore();
const replaceObjectMetadataItemIfDifferent = useCallback(
(
toSetObjectMetadataItems: Omit<
ObjectMetadataItem,
'readableFields' | 'updatableFields'
>[],
) => {
const currentUserWorkspace = store.get(currentUserWorkspaceState.atom);
if (!isDefined(currentUserWorkspace)) {
return;
}
const objectPermissionsByObjectMetadataId =
currentUserWorkspace.objectsPermissions.reduce(
(acc, objectPermission) => {
acc[objectPermission.objectMetadataId] = objectPermission;
return acc;
},
{} as Record<
string,
ObjectPermissions & { objectMetadataId: string }
>,
);
const newObjectMetadataItems = enrichObjectMetadataItemsWithPermissions({
objectMetadataItems: toSetObjectMetadataItems,
objectPermissionsByObjectMetadataId,
});
if (
!isDeeplyEqual(
store.get(objectMetadataItemsState.atom),
newObjectMetadataItems,
) &&
newObjectMetadataItems.length > 0
) {
store.set(objectMetadataItemsState.atom, newObjectMetadataItems);
}
if (store.get(isAppEffectRedirectEnabledState.atom) === false) {
store.set(isAppEffectRedirectEnabledState.atom, true);
}
return newObjectMetadataItems;
},
[store],
);
const refreshObjectMetadataItems = async () => {
const refreshObjectMetadataItems = useCallback(async () => {
const objectMetadataItemsResult =
await client.query<ObjectMetadataItemsQuery>({
query: FIND_MANY_OBJECT_METADATA_ITEMS,
@ -77,13 +24,23 @@ export const useRefreshObjectMetadataItems = (
fetchPolicy,
});
const objectMetadataItems =
const compositeObjects =
mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: objectMetadataItemsResult.data,
});
return replaceObjectMetadataItemIfDifferent(objectMetadataItems);
};
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(compositeObjects);
updateDraft('objectMetadataItems', flatObjects);
updateDraft('fieldMetadataItems', flatFields);
updateDraft('indexMetadataItems', flatIndexes);
applyChanges();
if (store.get(isAppEffectRedirectEnabledState.atom) === false) {
store.set(isAppEffectRedirectEnabledState.atom, true);
}
}, [client, fetchPolicy, store, updateDraft, applyChanges]);
return {
refreshObjectMetadataItems,

View file

@ -0,0 +1,13 @@
import { objectMetadataItemsSelector } from '@/metadata-store/states/objectMetadataItemsSelector';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const activeObjectNameSingularsSelector = createAtomSelector<string[]>({
key: 'activeObjectNameSingularsSelector',
get: ({ get }) => {
const flatObjects = get(objectMetadataItemsSelector);
return flatObjects
.filter((flatObject) => flatObject.isActive)
.map((flatObject) => flatObject.nameSingular);
},
});

View file

@ -1,5 +1,5 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getFilterFilterableFieldMetadataItems } from '@/object-metadata/utils/getFilterFilterableFieldMetadataItems';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
@ -17,7 +17,7 @@ export const availableFieldMetadataItemsForFilterFamilySelector =
({ objectMetadataItemId }: { objectMetadataItemId: string }) =>
({ get }) => {
const currentWorkspace = get(currentWorkspaceState);
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === objectMetadataItemId,

View file

@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { filterSortableFieldMetadataItems } from '@/object-metadata/utils/filterSortableFieldMetadataItems';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
@ -13,7 +13,7 @@ export const availableFieldMetadataItemsForSortFamilySelector =
get:
({ objectMetadataItemId }: { objectMetadataItemId: string }) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === objectMetadataItemId,

View file

@ -1,5 +1,5 @@
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
import { findById, isDefined } from 'twenty-shared/utils';
@ -8,7 +8,7 @@ export const fieldMetadataItemByIdSelector = createAtomFamilySelector({
get:
({ fieldMetadataItemId }: { fieldMetadataItemId: string }) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
const flattenedFieldMetadataItems = get(
flattenedFieldMetadataItemsSelector,
);

View file

@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
@ -7,7 +7,7 @@ export const flattenedFieldMetadataItemsSelector = createAtomSelector<
>({
key: 'flattenedFieldMetadataItemsSelector',
get: ({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
return objectMetadataItems.flatMap(
(objectMetadataItem) => objectMetadataItem.fields,

View file

@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
@ -7,7 +7,7 @@ export const flattenedReadableFieldMetadataItemsSelector = createAtomSelector<
>({
key: 'flattenedReadableFieldMetadataItemsSelector',
get: ({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
return objectMetadataItems.flatMap(
(objectMetadataItem) => objectMetadataItem.readableFields,

View file

@ -1,7 +1,7 @@
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
import { findById, isDefined } from 'twenty-shared/utils';
@ -11,7 +11,7 @@ export const isFieldMetadataItemFilterableAndSortableSelector =
get:
({ fieldMetadataItemId }: { fieldMetadataItemId: string }) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
const flattenedFieldMetadataItems = get(
flattenedFieldMetadataItemsSelector,
);

View file

@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
import { findById, isDefined } from 'twenty-shared/utils';
@ -9,7 +9,7 @@ export const isFieldMetadataItemLabelIdentifierSelector =
get:
({ fieldMetadataItemId }: { fieldMetadataItemId: string }) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
const foundObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>

View file

@ -0,0 +1,18 @@
import { objectMetadataItemsSelector } from '@/metadata-store/states/objectMetadataItemsSelector';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
export const isSystemObjectByNameSingularFamilySelector =
createAtomFamilySelector<boolean, string>({
key: 'isSystemObjectByNameSingularFamilySelector',
get:
(nameSingular: string) =>
({ get }) => {
const flatObjects = get(objectMetadataItemsSelector);
return (
flatObjects.find(
(flatObject) => flatObject.nameSingular === nameSingular,
)?.isSystem ?? false
);
},
});

View file

@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
import { isDefined } from 'twenty-shared/utils';
@ -9,7 +9,7 @@ export const labelIdentifierFieldMetadataItemSelector =
get:
({ objectMetadataItemId }: { objectMetadataItemId: string }) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>

View file

@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
@ -15,7 +15,7 @@ export const objectMetadataItemFamilySelector = createAtomFamilySelector<
get:
({ objectNameType, objectName }: ObjectMetadataItemSelector) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
if (objectNameType === 'singular') {
return (

View file

@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
@ -7,7 +7,7 @@ export const objectMetadataItemsByNamePluralMapSelector = createAtomSelector<
>({
key: 'objectMetadataItemsByNamePluralMapSelector',
get: ({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
return new Map(
objectMetadataItems.map((objectMetadataItem) => [

View file

@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
@ -7,7 +7,7 @@ export const objectMetadataItemsByNameSingularMapSelector = createAtomSelector<
>({
key: 'objectMetadataItemsByNameSingularMapSelector',
get: ({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
return new Map(
objectMetadataItems.map((objectMetadataItem) => [

View file

@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
@ -8,7 +8,7 @@ export const objectMetadataItemsBySingularNameSelector =
get:
(objectNameSingulars: string[]) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
return objectNameSingulars.flatMap((objectNameSingular) => {
const found = objectMetadataItems.find(

View file

@ -1,7 +1,10 @@
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const objectMetadataItemsState = createAtomState<ObjectMetadataItem[]>({
export const objectMetadataItemsState = createAtomSelector<
ObjectMetadataItem[]
>({
key: 'objectMetadataItemsState',
defaultValue: [],
get: ({ get }) => get(objectMetadataItemsWithFieldsSelector),
});

View file

@ -0,0 +1,95 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { fieldMetadataItemsSelector } from '@/metadata-store/states/fieldMetadataItemsSelector';
import { indexMetadataItemsSelector } from '@/metadata-store/states/indexMetadataItemsSelector';
import { objectMetadataItemsSelector } from '@/metadata-store/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getNonReadableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonReadableFieldMetadataIdsFromObjectPermissions';
import { getNonUpdatableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonUpdatableFieldMetadataIdsFromObjectPermissions';
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const objectMetadataItemsWithFieldsSelector = createAtomSelector<
ObjectMetadataItem[]
>({
key: 'objectMetadataItemsWithFieldsSelector',
get: ({ get }) => {
const flatObjects = get(objectMetadataItemsSelector);
const allFlatFields = get(fieldMetadataItemsSelector);
const allFlatIndexes = get(indexMetadataItemsSelector);
const currentUserWorkspace = get(currentUserWorkspaceState);
const fieldsByObjectId = new Map<
string,
(typeof allFlatFields)[number][]
>();
for (const field of allFlatFields) {
const existing = fieldsByObjectId.get(field.objectMetadataId);
if (isDefined(existing)) {
existing.push(field);
} else {
fieldsByObjectId.set(field.objectMetadataId, [field]);
}
}
const indexesByObjectId = new Map<
string,
(typeof allFlatIndexes)[number][]
>();
for (const index of allFlatIndexes) {
const existing = indexesByObjectId.get(index.objectMetadataId);
if (isDefined(existing)) {
existing.push(index);
} else {
indexesByObjectId.set(index.objectMetadataId, [index]);
}
}
const objectPermissionsByObjectMetadataId =
currentUserWorkspace?.objectsPermissions.reduce(
(accumulator, objectPermission) => {
accumulator[objectPermission.objectMetadataId] = objectPermission;
return accumulator;
},
{} as Record<string, ObjectPermissions & { objectMetadataId: string }>,
) ?? {};
return flatObjects.map((flatObject) => {
const fields = fieldsByObjectId.get(flatObject.id) ?? [];
const indexMetadatas = indexesByObjectId.get(flatObject.id) ?? [];
const objectPermissions = getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: flatObject.id,
});
const nonReadableFieldMetadataIds =
getNonReadableFieldMetadataIdsFromObjectPermissions({
objectPermissions,
});
const nonUpdatableFieldMetadataIds =
getNonUpdatableFieldMetadataIdsFromObjectPermissions({
objectPermissions,
});
return {
...flatObject,
fields,
indexMetadatas,
readableFields: fields.filter(
(field) => !nonReadableFieldMetadataIds.includes(field.id),
),
updatableFields: fields.filter(
(field) => !nonUpdatableFieldMetadataIds.includes(field.id),
),
} satisfies ObjectMetadataItem;
});
},
});

View file

@ -1,13 +1,16 @@
import { renderHook } from '@testing-library/react';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
jotaiStore.set(objectMetadataItemsState.atom, generatedMockObjectMetadataItems);
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],

View file

@ -1,8 +1,8 @@
import { gql } from '@apollo/client';
import { renderHook, waitFor } from '@testing-library/react';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type RecordGqlFields } from '@/object-record/graphql/record-gql-fields/types/RecordGqlFields';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
import { useCombinedFindManyRecords } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecords';
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
@ -123,8 +123,8 @@ const renderUseCombinedFindManyRecordsHook = async ({
},
];
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);

View file

@ -1,7 +1,7 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
@ -32,8 +32,8 @@ const meta: Meta<typeof ObjectOptionsDropdownContent> = {
decorators: [
(Story) => {
useEffect(() => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
}, []);

View file

@ -1,7 +1,8 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { useEffect } from 'react';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
@ -15,7 +16,6 @@ import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
import { labelIdentifierFieldMetadataItemSelector } from '@/object-metadata/states/labelIdentifierFieldMetadataItemSelector';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
@ -53,7 +53,7 @@ const RelationFieldValueSetterEffect = () => {
'recordTableId',
);
const [, setObjectMetadataItems] = useAtomState(objectMetadataItemsState);
const { updateDraft, applyChanges } = useMetadataStore();
useEffect(() => {
setRecordStore(mockPerformance.entityValue);
@ -71,11 +71,18 @@ const RelationFieldValueSetterEffect = () => {
),
);
setObjectMetadataItems(generatedMockObjectMetadataItems);
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems);
updateDraft('objectMetadataItems', flatObjects);
updateDraft('fieldMetadataItems', flatFields);
updateDraft('indexMetadataItems', flatIndexes);
applyChanges();
}, [
setRecordStore,
setRelationRecordStore,
setObjectMetadataItems,
updateDraft,
applyChanges,
setCurrentRecordFields,
]);

View file

@ -1,8 +1,8 @@
import { act, renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { textfieldDefinition } from '@/object-record/record-field/ui/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
@ -26,8 +26,8 @@ import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockO
const recordTableId = 'record-table-id';
const Wrapper = ({ children }: { children: React.ReactNode }) => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);

View file

@ -1,8 +1,8 @@
import { act, renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { textfieldDefinition } from '@/object-record/record-field/ui/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
@ -26,8 +26,8 @@ import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockO
const recordTableId = 'record-table-id';
const Wrapper = ({ children }: { children: React.ReactNode }) => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);

View file

@ -4,8 +4,8 @@ import { type ReactNode } from 'react';
import { useIcons } from 'twenty-ui/display';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
@ -34,7 +34,7 @@ describe('useBuildSpreadSheetImportFields', () => {
getIcons: () => ({}),
});
jest.clearAllMocks();
jotaiStore.set(objectMetadataItemsState.atom, []);
setTestObjectMetadataItemsInMetadataStore(jotaiStore, []);
});
const createMockFieldMetadataItem = (

View file

@ -1,5 +1,5 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
import {
@ -33,8 +33,8 @@ const meta: Meta<typeof DashboardWidgetPlaceholder> = {
component: DashboardWidgetPlaceholder,
decorators: [
(Story) => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);

View file

@ -8,8 +8,8 @@ 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 { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@ -277,8 +277,8 @@ export const WithNumberChart: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -364,8 +364,8 @@ export const WithGaugeChart: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -455,8 +455,8 @@ export const WithBarChart: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -549,8 +549,8 @@ export const SmallWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -647,8 +647,8 @@ export const MediumWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -745,8 +745,8 @@ export const LargeWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -839,8 +839,8 @@ export const WideWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -937,8 +937,8 @@ export const TallWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1031,8 +1031,8 @@ export const WithManyToOneRelationFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1150,8 +1150,8 @@ export const WithOneToManyRelationFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1261,8 +1261,8 @@ export const OneToManyRelationFieldWidgetWithSeeAllButton: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1399,8 +1399,8 @@ export const OnMobile: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1497,8 +1497,8 @@ export const InSidePanel: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1656,8 +1656,8 @@ export const Catalog: CatalogStory<Story, typeof WidgetRenderer> = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);

View file

@ -5,8 +5,8 @@ import { MemoryRouter } from 'react-router-dom';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
@ -356,8 +356,8 @@ export const TextFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -450,8 +450,8 @@ export const AddressFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -547,8 +547,8 @@ export const NumberFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -641,8 +641,8 @@ export const LinkFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -735,8 +735,8 @@ export const ManyToOneRelationFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -839,8 +839,8 @@ export const OneToManyRelationFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -935,8 +935,8 @@ export const BooleanFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1028,8 +1028,8 @@ export const CurrencyFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1121,8 +1121,8 @@ export const EmailsFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1215,8 +1215,8 @@ export const PhonesFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1309,8 +1309,8 @@ export const SelectFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1404,8 +1404,8 @@ export const MultiSelectFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1502,8 +1502,8 @@ export const TimelineActivityRelationFieldWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1601,8 +1601,8 @@ export const ManyToOneRelationCardWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1713,8 +1713,8 @@ export const OneToManyRelationCardWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1808,8 +1808,8 @@ export const TimelineActivityRelationCardWidget: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -1969,8 +1969,8 @@ export const OneToManyRelationCardWidgetWithProgressiveLoading: Story = {
deletedAt: null,
};
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);

View file

@ -6,8 +6,8 @@ import { expect, waitFor, within } from 'storybook/test';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
@ -321,8 +321,8 @@ export const WithViewFieldGroups: Story = {
companyObjectMetadataItem.id,
);
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -434,8 +434,8 @@ export const WithInlineViewFields: Story = {
companyObjectMetadataItem.id,
);
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
@ -526,8 +526,8 @@ export const Empty: Story = {
companyObjectMetadataItem.id,
);
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);

View file

@ -4,8 +4,8 @@ import { Provider as JotaiProvider } from 'jotai';
import { type ChartConfiguration } from '@/side-panel/pages/page-layout/types/ChartConfiguration';
import { CHART_CONFIGURATION_SETTING_IDS } from '@/side-panel/pages/page-layout/types/ChartConfigurationSettingIds';
import { type TypedBarChartConfiguration } from '@/side-panel/pages/page-layout/types/TypedBarChartConfiguration';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import {
AggregateOperations,
AxisNameDisplay,
@ -80,7 +80,9 @@ const buildBarChartConfiguration = (
}) as TypedBarChartConfiguration;
const renderUseChartSettingsValues = (configuration: ChartConfiguration) => {
jotaiStore.set(objectMetadataItemsState.atom, [mockObjectMetadataItem]);
setTestObjectMetadataItemsInMetadataStore(jotaiStore, [
mockObjectMetadataItem,
]);
return renderHook(
() =>
@ -411,7 +413,9 @@ describe('useChartSettingsValues', () => {
it('should handle missing objectMetadataItem gracefully', () => {
const config = buildBarChartConfiguration({});
jotaiStore.set(objectMetadataItemsState.atom, [mockObjectMetadataItem]);
setTestObjectMetadataItemsInMetadataStore(jotaiStore, [
mockObjectMetadataItem,
]);
const { result } = renderHook(
() =>

View file

@ -3,8 +3,8 @@ import { useEffect } from 'react';
import { expect, within } from 'storybook/test';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { SettingsPath } from 'twenty-shared/types';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
@ -56,8 +56,8 @@ const meta: Meta<typeof NavigationDrawer> = {
currentWorkspaceMemberState,
);
useEffect(() => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
setCurrentWorkspaceMember(mockedWorkspaceMemberData);

View file

@ -6,7 +6,6 @@ import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-rec
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown } from '@/views/hooks/useInitializeFilterOnFieldMetadataItemFromViewBarFilterDropdown';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import {
jotaiStore,
resetJotaiStore,
@ -26,6 +25,7 @@ import { ViewBarFilterDropdownIds } from '@/views/constants/ViewBarFilterDropdow
import { getFilterTypeFromFieldType } from 'twenty-shared/utils';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
const mockPushFocusItemToFocusStack = jest.fn();
@ -46,8 +46,8 @@ const personCreatedAtFieldMetadataItemMock =
);
const wrapper = ({ children }: { children: React.ReactNode }) => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
return (

View file

@ -1,5 +1,5 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState';
import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar';
@ -22,7 +22,7 @@ const createMockObjectMetadataItem = (fields: any[]) => ({
const createWrapper = (objectMetadataItems: any[]) => {
return ({ children }: { children: ReactNode }) => {
jotaiStore.set(objectMetadataItemsState.atom, objectMetadataItems);
setTestObjectMetadataItemsInMetadataStore(jotaiStore, objectMetadataItems);
jotaiStore.set(
viewObjectMetadataIdComponentState.atomFamily({
instanceId: mockViewInstanceId,
@ -80,7 +80,7 @@ describe('useGetAvailableFieldsForCalendar', () => {
});
expect(result.current.availableFieldsForCalendar).toHaveLength(2);
expect(result.current.availableFieldsForCalendar).toEqual([
expect(result.current.availableFieldsForCalendar).toMatchObject([
{
id: '1',
type: FieldMetadataType.DATE,

View file

@ -1,5 +1,5 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState';
import { useGetAvailableFieldsToGroupRecordsBy } from '@/views/view-picker/hooks/useGetAvailableFieldsToGroupRecordsBy';
@ -22,7 +22,7 @@ const createMockObjectMetadataItem = (fields: any[]) => ({
const createWrapper = (objectMetadataItems: any[]) => {
return ({ children }: { children: ReactNode }) => {
jotaiStore.set(objectMetadataItemsState.atom, objectMetadataItems);
setTestObjectMetadataItemsInMetadataStore(jotaiStore, objectMetadataItems);
jotaiStore.set(
viewObjectMetadataIdComponentState.atomFamily({
instanceId: mockViewInstanceId,

View file

@ -10,9 +10,7 @@ import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useFetchAndLoadIndexViews } from '@/metadata-store/hooks/useFetchAndLoadIndexViews';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@ -22,7 +20,7 @@ import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { Trans, useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useStore } from 'jotai';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
@ -73,9 +71,7 @@ export const CreateWorkspace = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const setNextOnboardingStatus = useSetNextOnboardingStatus();
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { updateDraft, applyChanges } = useMetadataStore();
const { fetchAndLoadIndexViews } = useFetchAndLoadIndexViews();
const store = useStore();
const { loadCurrentUser } = useLoadCurrentUser();
const [activateWorkspace] = useMutation(ActivateWorkspaceDocument);
@ -132,10 +128,6 @@ export const CreateWorkspace = () => {
await refreshObjectMetadataItems();
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objectMetadataItems', loadedObjects);
applyChanges();
await fetchAndLoadIndexViews();
await loadCurrentUser();
@ -153,9 +145,6 @@ export const CreateWorkspace = () => {
enqueueErrorSnackBar,
loadCurrentUser,
refreshObjectMetadataItems,
updateDraft,
applyChanges,
store,
fetchAndLoadIndexViews,
setNextOnboardingStatus,
t,

View file

@ -1,8 +1,8 @@
import { type ReactNode, useEffect, useState } from 'react';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
export const JestObjectMetadataItemSetter = ({
@ -12,15 +12,20 @@ export const JestObjectMetadataItemSetter = ({
children: ReactNode;
objectMetadataItems?: ObjectMetadataItem[];
}) => {
const setObjectMetadataItems = useSetAtomState(objectMetadataItemsState);
const { updateDraft, applyChanges } = useMetadataStore();
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
setObjectMetadataItems(
objectMetadataItems ?? generatedMockObjectMetadataItems,
);
const items = objectMetadataItems ?? generatedMockObjectMetadataItems;
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(items);
updateDraft('objectMetadataItems', flatObjects);
updateDraft('fieldMetadataItems', flatFields);
updateDraft('indexMetadataItems', flatIndexes);
applyChanges();
setIsLoaded(true);
}, [objectMetadataItems, setObjectMetadataItems]);
}, [objectMetadataItems, updateDraft, applyChanges]);
return isLoaded ? <>{children}</> : null;
};

View file

@ -0,0 +1,32 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type createStore } from 'jotai';
type JotaiStore = ReturnType<typeof createStore>;
export const setTestObjectMetadataItemsInMetadataStore = (
store: JotaiStore,
objectMetadataItems: ObjectMetadataItem[],
) => {
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(objectMetadataItems);
store.set(metadataStoreState.atomFamily('objectMetadataItems'), {
current: flatObjects,
draft: [],
status: 'up-to-date',
});
store.set(metadataStoreState.atomFamily('fieldMetadataItems'), {
current: flatFields,
draft: [],
status: 'up-to-date',
});
store.set(metadataStoreState.atomFamily('indexMetadataItems'), {
current: flatIndexes,
draft: [],
status: 'up-to-date',
});
};

View file

@ -88,8 +88,12 @@ export default defineConfig(({ mode }) => {
include: [path.resolve(__dirname, 'src') + '/**/*.{ts,tsx}'],
exclude: [
'**/generated-metadata/**',
'**/testing/mock-data/generated/**',
'**/testing/**',
'**/testing/mock-data/**',
'**/testing/jest/**',
'**/testing/hooks/**',
'**/testing/utils/**',
'**/testing/constants/**',
'**/testing/cache/**',
'**/*.test.{ts,tsx}',
'**/*.spec.{ts,tsx}',
'**/*.stories.{ts,tsx}',