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
@@ -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,