Update store on data model mutation (#18684)

- enrich SSE events with relations
- remove queries from sse metadata events
- on sse event, manage store
- on object/field metadata changes, manage store

TODO left:
- fix other metadata items

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thomas Trompette
2026-03-17 19:32:19 +01:00
committed by GitHub
parent e62dfae741
commit 994215e0dc
35 changed files with 1399 additions and 925 deletions
@@ -1,11 +1,14 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { type MetadataEntityKey } from '@/metadata-store/states/metadataStoreState';
import { type MetadataEntityTypeMap } from '@/metadata-store/types/MetadataEntityTypeMap';
import { mapAllMetadataNameToEntityKey } from '@/metadata-store/utils/mapAllMetadataNameToEntityKey';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
type AnyMetadataEntity = MetadataEntityTypeMap[MetadataEntityKey];
export const MetadataStoreSSEEffect = () => {
const store = useStore();
const { addToDraft, removeFromDraft, applyChanges } = useMetadataStore();
useListenToMetadataOperationBrowserEvent({
onMetadataOperationBrowserEvent: (eventDetail) => {
@@ -15,12 +18,42 @@ export const MetadataStoreSSEEffect = () => {
return;
}
patchMetadataStoreFromSSEEvent({
store,
entityKey,
operation: eventDetail.operation,
updatedCollectionHash: eventDetail.updatedCollectionHash,
});
const collectionHash = eventDetail.updatedCollectionHash;
switch (eventDetail.operation.type) {
case 'create': {
addToDraft({
key: entityKey,
items: [
eventDetail.operation
.createdRecord as unknown as AnyMetadataEntity,
],
collectionHash,
});
break;
}
case 'update': {
addToDraft({
key: entityKey,
items: [
eventDetail.operation
.updatedRecord as unknown as AnyMetadataEntity,
],
collectionHash,
});
break;
}
case 'delete': {
removeFromDraft({
key: entityKey,
itemIds: [eventDetail.operation.deletedRecordId],
collectionHash,
});
break;
}
}
applyChanges();
},
});
@@ -6,7 +6,7 @@ import { useCallback } from 'react';
const MOCKED_COLLECTION_HASH = 'mocked';
export const useLoadMockedMinimalMetadata = () => {
const { updateDraft, applyChanges, resetMetadataStore } = useMetadataStore();
const { replaceDraft, applyChanges, resetMetadataStore } = useMetadataStore();
const loadMockedMinimalMetadata = useCallback(async () => {
resetMetadataStore();
@@ -25,9 +25,9 @@ export const useLoadMockedMinimalMetadata = () => {
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems);
updateDraft('objectMetadataItems', flatObjects, MOCKED_COLLECTION_HASH);
updateDraft('fieldMetadataItems', flatFields, MOCKED_COLLECTION_HASH);
updateDraft('indexMetadataItems', flatIndexes, MOCKED_COLLECTION_HASH);
replaceDraft('objectMetadataItems', flatObjects, MOCKED_COLLECTION_HASH);
replaceDraft('fieldMetadataItems', flatFields, MOCKED_COLLECTION_HASH);
replaceDraft('indexMetadataItems', flatIndexes, MOCKED_COLLECTION_HASH);
const {
flatViews,
@@ -39,26 +39,30 @@ export const useLoadMockedMinimalMetadata = () => {
flatViewFieldGroups,
} = splitViewWithRelated(mockedViews);
updateDraft('views', flatViews, MOCKED_COLLECTION_HASH);
updateDraft('viewFields', flatViewFields, MOCKED_COLLECTION_HASH);
updateDraft('viewFilters', flatViewFilters, MOCKED_COLLECTION_HASH);
updateDraft('viewSorts', flatViewSorts, MOCKED_COLLECTION_HASH);
updateDraft('viewGroups', flatViewGroups, MOCKED_COLLECTION_HASH);
updateDraft(
replaceDraft('views', flatViews, MOCKED_COLLECTION_HASH);
replaceDraft('viewFields', flatViewFields, MOCKED_COLLECTION_HASH);
replaceDraft('viewFilters', flatViewFilters, MOCKED_COLLECTION_HASH);
replaceDraft('viewSorts', flatViewSorts, MOCKED_COLLECTION_HASH);
replaceDraft('viewGroups', flatViewGroups, MOCKED_COLLECTION_HASH);
replaceDraft(
'viewFilterGroups',
flatViewFilterGroups,
MOCKED_COLLECTION_HASH,
);
updateDraft('viewFieldGroups', flatViewFieldGroups, MOCKED_COLLECTION_HASH);
replaceDraft(
'viewFieldGroups',
flatViewFieldGroups,
MOCKED_COLLECTION_HASH,
);
updateDraft(
replaceDraft(
'navigationMenuItems',
mockedNavigationMenuItems,
MOCKED_COLLECTION_HASH,
);
applyChanges();
}, [updateDraft, applyChanges, resetMetadataStore]);
}, [replaceDraft, applyChanges, resetMetadataStore]);
return { loadMockedMinimalMetadata };
};
@@ -54,7 +54,7 @@ const hasOverlap = (
export const useLoadStaleMetadataEntities = () => {
const client = useApolloClient();
const store = useStore();
const { updateDraft, applyChanges } = useMetadataStore();
const { replaceDraft, applyChanges } = useMetadataStore();
const loadStaleMetadataEntities = useCallback(
async (staleEntityKeys: MetadataEntityKey[]) => {
@@ -80,9 +80,9 @@ export const useLoadStaleMetadataEntities = () => {
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(compositeObjects);
updateDraft('objectMetadataItems', flatObjects);
updateDraft('fieldMetadataItems', flatFields);
updateDraft('indexMetadataItems', flatIndexes);
replaceDraft('objectMetadataItems', flatObjects);
replaceDraft('fieldMetadataItems', flatFields);
replaceDraft('indexMetadataItems', flatIndexes);
}),
);
}
@@ -116,13 +116,13 @@ export const useLoadStaleMetadataEntities = () => {
flatViewFieldGroups,
} = splitViewWithRelated(allViews);
updateDraft('views', flatViews);
updateDraft('viewFields', flatViewFields);
updateDraft('viewFilters', flatViewFilters);
updateDraft('viewSorts', flatViewSorts);
updateDraft('viewGroups', flatViewGroups);
updateDraft('viewFilterGroups', flatViewFilterGroups);
updateDraft('viewFieldGroups', flatViewFieldGroups);
replaceDraft('views', flatViews);
replaceDraft('viewFields', flatViewFields);
replaceDraft('viewFilters', flatViewFilters);
replaceDraft('viewSorts', flatViewSorts);
replaceDraft('viewGroups', flatViewGroups);
replaceDraft('viewFilterGroups', flatViewFilterGroups);
replaceDraft('viewFieldGroups', flatViewFieldGroups);
}),
);
}
@@ -148,9 +148,9 @@ export const useLoadStaleMetadataEntities = () => {
flatPageLayoutWidgets,
} = splitPageLayoutWithRelated(transformed);
updateDraft('pageLayouts', flatPageLayouts);
updateDraft('pageLayoutTabs', flatPageLayoutTabs);
updateDraft('pageLayoutWidgets', flatPageLayoutWidgets);
replaceDraft('pageLayouts', flatPageLayouts);
replaceDraft('pageLayoutTabs', flatPageLayoutTabs);
replaceDraft('pageLayoutWidgets', flatPageLayoutWidgets);
}),
);
}
@@ -171,7 +171,10 @@ export const useLoadStaleMetadataEntities = () => {
logicFunctionsState.atom,
result.data.findManyLogicFunctions,
);
updateDraft('logicFunctions', result.data.findManyLogicFunctions);
replaceDraft(
'logicFunctions',
result.data.findManyLogicFunctions,
);
}),
);
}
@@ -188,7 +191,7 @@ export const useLoadStaleMetadataEntities = () => {
return;
}
updateDraft(
replaceDraft(
'navigationMenuItems',
result.data.navigationMenuItems,
);
@@ -199,7 +202,7 @@ export const useLoadStaleMetadataEntities = () => {
await Promise.all(fetchPromises);
applyChanges();
},
[client, store, updateDraft, applyChanges],
[client, store, replaceDraft, applyChanges],
);
return { loadStaleMetadataEntities };
@@ -8,6 +8,7 @@ import {
import { type MetadataEntityTypeMap } from '@/metadata-store/types/MetadataEntityTypeMap';
import { useStore, type createStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type JotaiStore = ReturnType<typeof createStore>;
@@ -58,7 +59,7 @@ const changeMetadataEntityAsUpToDate = (
export const useMetadataStore = () => {
const store = useStore();
const updateDraft = useCallback(
const replaceDraft = useCallback(
<K extends MetadataEntityKey>(
key: K,
data: MetadataEntityTypeMap[K][],
@@ -123,9 +124,159 @@ export const useMetadataStore = () => {
return { hasPersistedAnyMetadataEntity };
}, [store]);
const addToDraft = useCallback(
<K extends MetadataEntityKey>({
key,
items,
collectionHash,
}: {
key: K;
items: MetadataEntityTypeMap[K][];
collectionHash?: string;
}) => {
if (items.length === 0) {
return;
}
const entry = store.get(metadataStoreState.atomFamily(key));
const baseItems = (
entry.status === 'draft-pending' ? entry.draft : entry.current
) as Array<{ id: string }>;
let newItems = [...baseItems];
for (const item of items) {
const newItem = item as unknown as { id: string };
const existingIndex = newItems.findIndex(
(existing) => existing.id === newItem.id,
);
if (existingIndex >= 0) {
if (!isDeeplyEqual(newItems[existingIndex], newItem)) {
newItems[existingIndex] = newItem;
}
} else {
newItems = [...newItems, newItem];
}
}
store.set(metadataStoreState.atomFamily(key), {
...entry,
draft: newItems,
status: 'draft-pending' as const,
...(collectionHash !== undefined
? { draftCollectionHash: collectionHash }
: {}),
});
},
[store],
);
const updateInDraft = useCallback(
<K extends MetadataEntityKey>(
key: K,
items: MetadataEntityTypeMap[K][],
collectionHash?: string,
) => {
if (items.length === 0) {
return;
}
const entry = store.get(metadataStoreState.atomFamily(key));
const baseItems = (
entry.status === 'draft-pending' ? entry.draft : entry.current
) as Array<{ id: string }>;
let hasChanged = false;
const newItems = baseItems.map((existing) => {
const updatedItem = (items as unknown as Array<{ id: string }>).find(
(item) => item.id === existing.id,
);
if (!isDefined(updatedItem)) {
return existing;
}
const merged = { ...existing, ...updatedItem };
if (isDeeplyEqual(existing, merged)) {
return existing;
}
hasChanged = true;
return merged;
});
if (!hasChanged && collectionHash === undefined) {
return;
}
store.set(metadataStoreState.atomFamily(key), {
...entry,
draft: hasChanged ? newItems : baseItems,
status: 'draft-pending' as const,
...(collectionHash !== undefined
? { draftCollectionHash: collectionHash }
: {}),
});
},
[store],
);
const removeFromDraft = useCallback(
<K extends MetadataEntityKey>({
key,
itemIds,
collectionHash,
}: {
key: K;
itemIds: string[];
collectionHash?: string;
}) => {
if (itemIds.length === 0) {
return;
}
const entry = store.get(metadataStoreState.atomFamily(key));
const baseItems = (
entry.status === 'draft-pending' ? entry.draft : entry.current
) as Array<{ id: string }>;
const idsToDelete = new Set(itemIds);
const newItems = baseItems.filter(
(existing) => !idsToDelete.has(existing.id),
);
if (
newItems.length === baseItems.length &&
collectionHash === undefined
) {
return;
}
store.set(metadataStoreState.atomFamily(key), {
...entry,
draft: newItems,
status: 'draft-pending' as const,
...(collectionHash !== undefined
? { draftCollectionHash: collectionHash }
: {}),
});
},
[store],
);
const reset = useCallback(() => {
resetMetadataStore(store);
}, [store]);
return { updateDraft, applyChanges, resetMetadataStore: reset };
return {
replaceDraft,
applyChanges,
addToDraft,
updateInDraft,
removeFromDraft,
resetMetadataStore: reset,
};
};
@@ -1,74 +0,0 @@
import {
metadataStoreState,
type MetadataEntityKey,
} from '@/metadata-store/states/metadataStoreState';
import { type createStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
type JotaiStore = ReturnType<typeof createStore>;
type SSEEventOperation =
| {
type: 'create';
createdRecord: Record<string, unknown>;
}
| {
type: 'update';
updatedRecord: Record<string, unknown>;
}
| {
type: 'delete';
deletedRecordId: string;
};
export const patchMetadataStoreFromSSEEvent = ({
store,
entityKey,
operation,
updatedCollectionHash,
}: {
store: JotaiStore;
entityKey: MetadataEntityKey;
operation: SSEEventOperation;
updatedCollectionHash?: string;
}) => {
const entry = store.get(metadataStoreState.atomFamily(entityKey));
const currentItems = entry.current as Array<{ id: string }>;
const hashUpdate = isDefined(updatedCollectionHash)
? { currentCollectionHash: updatedCollectionHash }
: {};
switch (operation.type) {
case 'create': {
store.set(metadataStoreState.atomFamily(entityKey), {
...entry,
...hashUpdate,
current: [...currentItems, operation.createdRecord],
});
break;
}
case 'update': {
store.set(metadataStoreState.atomFamily(entityKey), {
...entry,
...hashUpdate,
current: currentItems.map((item) =>
item.id === (operation.updatedRecord as { id: string }).id
? { ...item, ...operation.updatedRecord }
: item,
),
});
break;
}
case 'delete': {
store.set(metadataStoreState.atomFamily(entityKey), {
...entry,
...hashUpdate,
current: currentItems.filter(
(item) => item.id !== operation.deletedRecordId,
),
});
break;
}
}
};
@@ -1,12 +1,12 @@
import { useMutation } from '@apollo/client/react';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useMutation } from '@apollo/client/react';
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const useCreateNavigationMenuItem = () => {
const { navigationMenuItems, currentWorkspaceMemberId } =
@@ -14,7 +14,7 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => {
const store = useStore();
const apolloCoreClient = useApolloCoreClient();
const cache = apolloCoreClient.cache;
const { updateDraft, applyChanges } = useMetadataStore();
const { replaceDraft, applyChanges } = useMetadataStore();
const removeNavigationMenuItemsByTargetRecordIds = useCallback(
(targetRecordIds: string[]) => {
@@ -31,7 +31,7 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => {
!targetRecordIdsSet.has(item.targetRecordId),
);
updateDraft('navigationMenuItems', updatedNavigationMenuItems);
replaceDraft('navigationMenuItems', updatedNavigationMenuItems);
applyChanges();
cache.updateQuery(
@@ -48,7 +48,7 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => {
},
);
},
[cache, store, updateDraft, applyChanges],
[cache, store, replaceDraft, applyChanges],
);
return {
@@ -19,6 +19,71 @@ export const CREATE_ONE_OBJECT_METADATA_ITEM = gql`
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
isLabelSyncedWithName
fieldsList {
id
universalIdentifier
type
name
label
description
icon
isCustom
isActive
isSystem
isUIReadOnly
isNullable
isUnique
createdAt
updatedAt
defaultValue
options
settings
isLabelSyncedWithName
morphId
applicationId
relation {
type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata {
id
name
}
}
morphRelations {
type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata {
id
name
}
}
}
}
}
`;
@@ -46,6 +111,48 @@ export const CREATE_ONE_FIELD_METADATA_ITEM = gql`
object {
id
}
relation {
type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata {
id
name
}
}
morphRelations {
type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata {
id
name
}
}
}
}
`;
@@ -1,5 +1,6 @@
import {
CreateOneObjectMetadataItemDocument,
FindManyNavigationMenuItemsDocument,
FindManyViewsDocument,
} from '~/generated-metadata/graphql';
@@ -7,6 +8,9 @@ export const query = CreateOneObjectMetadataItemDocument;
export const findManyViewsQuery = FindManyViewsDocument;
export const findManyNavigationMenuItemsQuery =
FindManyNavigationMenuItemsDocument;
export const variables = {
input: {
object: {
@@ -35,4 +39,6 @@ export const responseData = {
updatedAt: '',
labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1',
imageIdentifierFieldMetadataId: '',
isLabelSyncedWithName: false,
fieldsList: [],
};
@@ -4,6 +4,7 @@ import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreat
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import {
findManyNavigationMenuItemsQuery,
findManyViewsQuery,
query,
responseData,
@@ -63,6 +64,17 @@ const mocks = [
},
})),
},
{
request: {
query: findManyNavigationMenuItemsQuery,
variables: {},
},
result: jest.fn(() => ({
data: {
navigationMenuItems: [],
},
})),
},
];
const Wrapper = getJestMetadataAndApolloMocksWrapper({
@@ -5,10 +5,13 @@ import {
} from '~/generated-metadata/graphql';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { CrudOperationType } from 'twenty-shared/types';
export const useCreateOneFieldMetadataItem = () => {
@@ -18,6 +21,7 @@ export const useCreateOneFieldMetadataItem = () => {
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const { addToDraft, applyChanges } = useMetadataStore();
const createOneFieldMetadataItem = async (
input: CreateFieldInput,
@@ -35,6 +39,23 @@ export const useCreateOneFieldMetadataItem = () => {
},
});
const createdField = response.data?.createOneField;
if (isDefined(createdField)) {
const { __typename, object, ...fieldData } = createdField;
addToDraft({
key: 'fieldMetadataItems',
items: [
{
...fieldData,
objectMetadataId: object?.id ?? input.objectMetadataId,
} as FlatFieldMetadataItem,
],
});
applyChanges();
}
return {
status: 'successful',
response,
@@ -1,22 +1,32 @@
import { useMutation } from '@apollo/client/react';
import { useApolloClient, useMutation } from '@apollo/client/react';
import {
type CreateObjectInput,
CreateOneObjectMetadataItemDocument,
FindManyNavigationMenuItemsDocument,
FindManyViewsDocument,
} from '~/generated-metadata/graphql';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem';
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const useCreateOneObjectMetadataItem = () => {
const [createOneObjectMetadataItemMutation] = useMutation(
CreateOneObjectMetadataItemDocument,
);
const client = useApolloClient();
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const { addToDraft, replaceDraft, applyChanges } = useMetadataStore();
const createOneObjectMetadataItem = async (
input: CreateObjectInput,
@@ -32,6 +42,73 @@ export const useCreateOneObjectMetadataItem = () => {
},
});
const createdObject = createdObjectMetadata.data?.createOneObject;
if (isDefined(createdObject)) {
const {
__typename: _objectTypename,
fieldsList,
...objectData
} = createdObject;
addToDraft({
key: 'objectMetadataItems',
items: [objectData as FlatObjectMetadataItem],
});
const flatFields = fieldsList.map((field) => {
const { __typename: _fieldTypename, ...fieldData } = field;
return {
...fieldData,
objectMetadataId: createdObject.id,
} as FlatFieldMetadataItem;
});
addToDraft({ key: 'fieldMetadataItems', items: flatFields });
applyChanges();
const [viewsResult, navItemsResult] = await Promise.all([
client.query({
query: FindManyViewsDocument,
variables: { objectMetadataId: createdObject.id },
fetchPolicy: 'network-only',
}),
client.query({
query: FindManyNavigationMenuItemsDocument,
fetchPolicy: 'network-only',
}),
]);
const fetchedViews = viewsResult.data?.getViews ?? [];
const {
flatViews,
flatViewFields,
flatViewFilters,
flatViewSorts,
flatViewGroups,
flatViewFilterGroups,
flatViewFieldGroups,
} = splitViewWithRelated(fetchedViews);
addToDraft({ key: 'views', items: flatViews });
addToDraft({ key: 'viewFields', items: flatViewFields });
addToDraft({ key: 'viewFilters', items: flatViewFilters });
addToDraft({ key: 'viewSorts', items: flatViewSorts });
addToDraft({ key: 'viewGroups', items: flatViewGroups });
addToDraft({ key: 'viewFilterGroups', items: flatViewFilterGroups });
addToDraft({ key: 'viewFieldGroups', items: flatViewFieldGroups });
replaceDraft(
'navigationMenuItems',
navItemsResult.data?.navigationMenuItems ?? [],
);
applyChanges();
}
return {
status: 'successful',
response: createdObjectMetadata,
@@ -3,6 +3,7 @@ import { DeleteOneFieldMetadataItemDocument } from '~/generated-metadata/graphql
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { recordIndexGroupAggregateFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateFieldMetadataItemComponentState';
import { recordIndexGroupAggregateOperationComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateOperationComponentState';
@@ -21,6 +22,7 @@ export const useDeleteOneFieldMetadataItem = () => {
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const { removeFromDraft, applyChanges } = useMetadataStore();
const setRecordIndexGroupAggregateOperation = useSetAtomComponentState(
recordIndexGroupAggregateOperationComponentState,
@@ -63,7 +65,10 @@ export const useDeleteOneFieldMetadataItem = () => {
},
});
// TODO: see if we can remove this lin altogether
removeFromDraft({ key: 'fieldMetadataItems', itemIds: [idToDelete] });
applyChanges();
// TODO: see if we can remove this line altogether
await resetRecordIndexKanbanAggregateOperation(idToDelete);
return {
@@ -2,6 +2,7 @@ import { useMutation } from '@apollo/client/react';
import { DeleteOneObjectMetadataItemDocument } from '~/generated-metadata/graphql';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
@@ -15,6 +16,7 @@ export const useDeleteOneObjectMetadataItem = () => {
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const { removeFromDraft, applyChanges } = useMetadataStore();
const deleteOneObjectMetadataItem = async (
idToDelete: string,
@@ -30,6 +32,9 @@ export const useDeleteOneObjectMetadataItem = () => {
},
});
removeFromDraft({ key: 'objectMetadataItems', itemIds: [idToDelete] });
applyChanges();
return {
status: 'successful',
response,
@@ -3,7 +3,7 @@ import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/split
import { useCallback } from 'react';
export const useLoadMockedObjectMetadataItems = () => {
const { updateDraft, applyChanges } = useMetadataStore();
const { replaceDraft, applyChanges } = useMetadataStore();
const loadMockedObjectMetadataItems = useCallback(async () => {
const { generatedMockObjectMetadataItems } = await import(
@@ -13,11 +13,11 @@ export const useLoadMockedObjectMetadataItems = () => {
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems);
updateDraft('objectMetadataItems', flatObjects);
updateDraft('fieldMetadataItems', flatFields);
updateDraft('indexMetadataItems', flatIndexes);
replaceDraft('objectMetadataItems', flatObjects);
replaceDraft('fieldMetadataItems', flatFields);
replaceDraft('indexMetadataItems', flatIndexes);
applyChanges();
}, [updateDraft, applyChanges]);
}, [replaceDraft, applyChanges]);
return {
loadMockedObjectMetadataItems,
@@ -5,12 +5,15 @@ import {
} from '~/generated-metadata/graphql';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem';
import { lastFieldMetadataItemUpdateState } from '@/object-metadata/states/lastFieldMetadataItemUpdateState';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { CrudOperationType } from 'twenty-shared/types';
import { v4 as uuidv4 } from 'uuid';
@@ -22,6 +25,7 @@ export const useUpdateOneFieldMetadataItem = () => {
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const { updateInDraft, applyChanges } = useMetadataStore();
const setLastFieldMetadataItemUpdate = useSetAtomState(
lastFieldMetadataItemUpdateState,
@@ -58,6 +62,20 @@ export const useUpdateOneFieldMetadataItem = () => {
},
});
const updatedField = response.data?.updateOneField;
if (isDefined(updatedField)) {
const { __typename, object, ...fieldData } = updatedField;
updateInDraft('fieldMetadataItems', [
{
...fieldData,
objectMetadataId: object?.id ?? objectMetadataId,
} as FlatFieldMetadataItem,
]);
applyChanges();
}
setLastFieldMetadataItemUpdate({
fieldMetadataItemId: fieldMetadataIdToUpdate,
objectMetadataId,
@@ -5,10 +5,13 @@ import {
} from '~/generated-metadata/graphql';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { CrudOperationType } from 'twenty-shared/types';
// TODO: Slice the Apollo store synchronously in the update function instead of subscribing, so we can use update after read in the same function call
@@ -19,6 +22,7 @@ export const useUpdateOneObjectMetadataItem = () => {
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const { updateInDraft, applyChanges } = useMetadataStore();
const updateOneObjectMetadataItem = async ({
idToUpdate,
@@ -39,6 +43,17 @@ export const useUpdateOneObjectMetadataItem = () => {
},
});
const updatedObject = response.data?.updateOneObject;
if (isDefined(updatedObject)) {
const { __typename, ...objectData } = updatedObject;
updateInDraft('objectMetadataItems', [
objectData as FlatObjectMetadataItem,
]);
applyChanges();
}
return {
status: 'successful',
response,
@@ -53,7 +53,7 @@ const RelationFieldValueSetterEffect = () => {
'recordTableId',
);
const { updateDraft, applyChanges } = useMetadataStore();
const { replaceDraft, applyChanges } = useMetadataStore();
useEffect(() => {
setRecordStore(mockPerformance.entityValue);
@@ -74,14 +74,14 @@ const RelationFieldValueSetterEffect = () => {
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems);
updateDraft('objectMetadataItems', flatObjects);
updateDraft('fieldMetadataItems', flatFields);
updateDraft('indexMetadataItems', flatIndexes);
replaceDraft('objectMetadataItems', flatObjects);
replaceDraft('fieldMetadataItems', flatFields);
replaceDraft('indexMetadataItems', flatIndexes);
applyChanges();
}, [
setRecordStore,
setRelationRecordStore,
updateDraft,
replaceDraft,
applyChanges,
setCurrentRecordFields,
]);
@@ -20,19 +20,17 @@ export const ON_EVENT_SUBSCRIPTION = gql`
}
queryIds
}
metadataEventsWithQueryIds {
metadataEvent {
type
metadataName
recordId
properties {
updatedFields
before
after
diff
}
metadataEvents {
type
metadataName
recordId
updatedCollectionHash
properties {
updatedFields
before
after
diff
}
queryIds
}
}
}
@@ -4,7 +4,6 @@ import { useCallback } from 'react';
import {
type AllMetadataName,
type MetadataEvent,
type MetadataEventWithQueryIds,
} from '~/generated-metadata/graphql';
const groupSseMetadataEventsByMetadataName = (
@@ -27,13 +26,9 @@ export const useDispatchMetadataEventsFromSseToBrowserEvents = <
T extends Record<string, unknown>,
>() => {
const dispatchMetadataEventsFromSseToBrowserEvents = useCallback(
(metadataEventsWithQueryIds: MetadataEventWithQueryIds[]) => {
const sseMetadataEvents = metadataEventsWithQueryIds.map(
(item) => item.metadataEvent,
);
(metadataEvents: MetadataEvent[]) => {
const eventsByMetadataName =
groupSseMetadataEventsByMetadataName(sseMetadataEvents);
groupSseMetadataEventsByMetadataName(metadataEvents);
for (const [metadataName, events] of eventsByMetadataName) {
const metadataOperationBrowserEvents =
@@ -98,8 +98,7 @@ export const useTriggerEventStreamCreation = () => {
const objectRecordEventsWithQueryIds =
eventSubscription?.objectRecordEventsWithQueryIds ?? [];
const metadataEventsWithQueryIds =
eventSubscription?.metadataEventsWithQueryIds ?? [];
const metadataEvents = eventSubscription?.metadataEvents ?? [];
const objectRecordEvents = objectRecordEventsWithQueryIds.map(
(item) => item.objectRecordEvent,
@@ -113,9 +112,7 @@ export const useTriggerEventStreamCreation = () => {
objectRecordEventsWithQueryIds,
);
dispatchMetadataEventsFromSseToBrowserEvents(
metadataEventsWithQueryIds,
);
dispatchMetadataEventsFromSseToBrowserEvents(metadataEvents);
},
error: (error) => {
captureException(error);
@@ -169,6 +166,11 @@ export const useTriggerEventStreamCreation = () => {
dispatchObjectRecordEventsFromSseToBrowserEvents(
objectRecordEventsWithQueryIds,
);
const metadataEvents =
result?.data?.onEventSubscription?.metadataEvents ?? [];
dispatchMetadataEventsFromSseToBrowserEvents(metadataEvents);
}
}
} catch (error) {