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:
+3
-3
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
+3
-3
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
+7
-7
@@ -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');
|
||||
});
|
||||
|
||||
+5
-2
@@ -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(
|
||||
() =>
|
||||
|
||||
+13
-15
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
+19
-62
@@ -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,
|
||||
|
||||
+13
@@ -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);
|
||||
},
|
||||
});
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
);
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
);
|
||||
|
||||
+2
-2
@@ -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) =>
|
||||
|
||||
+18
@@ -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
|
||||
);
|
||||
},
|
||||
});
|
||||
+2
-2
@@ -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) =>
|
||||
|
||||
+2
-2
@@ -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 (
|
||||
|
||||
+2
-2
@@ -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) => [
|
||||
|
||||
+2
-2
@@ -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) => [
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+6
-3
@@ -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),
|
||||
});
|
||||
|
||||
+95
@@ -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;
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user