From 5982a5a8ba5f797ec9680414027888482f3e476c Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:46:56 +0100 Subject: [PATCH] Aggregate queries and field metadata deletion (#9660) --- .../hooks/useDeleteOneFieldMetadataItem.ts | 31 ++++- .../field-metadata/field-metadata.module.ts | 2 + .../field-metadata/field-metadata.service.ts | 7 + .../src/modules/view/services/view.service.ts | 23 ++++ ...ete-one-field-metadata.integration-spec.ts | 121 ++++++++++++++++++ ...ate-one-field-metadata.integration-spec.ts | 4 +- ...create-custom-text-field-metadata.util.ts} | 2 +- 7 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 packages/twenty-server/test/integration/metadata/suites/field-metadata/delete-one-field-metadata.integration-spec.ts rename packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/{create-test-field-metadata.util.ts => create-custom-text-field-metadata.util.ts} (94%) diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts index 20985212a..82a7e16c7 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts @@ -1,13 +1,15 @@ -import { useMutation } from '@apollo/client'; +import { useApolloClient, useMutation } from '@apollo/client'; import { DeleteOneFieldMetadataItemMutation, DeleteOneFieldMetadataItemMutationVariables, } from '~/generated-metadata/graphql'; -import { DELETE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations'; - import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; +import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; +import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { useRecoilState } from 'recoil'; +import { DELETE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations'; import { useApolloMetadataClient } from './useApolloMetadataClient'; export const useDeleteOneFieldMetadataItem = () => { @@ -23,6 +25,27 @@ export const useDeleteOneFieldMetadataItem = () => { const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems('network-only'); + const [ + recordIndexKanbanAggregateOperation, + setRecordIndexKanbanAggregateOperation, + ] = useRecoilState(recordIndexKanbanAggregateOperationState); + + const apolloClient = useApolloClient(); + + const resetRecordIndexKanbanAggregateOperation = async ( + idToDelete: DeleteOneFieldMetadataItemMutationVariables['idToDelete'], + ) => { + if (recordIndexKanbanAggregateOperation?.fieldMetadataId === idToDelete) { + setRecordIndexKanbanAggregateOperation({ + operation: AGGREGATE_OPERATIONS.count, + fieldMetadataId: null, + }); + } + await apolloClient.refetchQueries({ + include: ['FindManyViews'], + }); + }; + const deleteOneFieldMetadataItem = async ( idToDelete: DeleteOneFieldMetadataItemMutationVariables['idToDelete'], ) => { @@ -32,6 +55,8 @@ export const useDeleteOneFieldMetadataItem = () => { }, }); + await resetRecordIndexKanbanAggregateOperation(idToDelete); + await refreshObjectMetadataItems(); return result; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts index 736d557c6..9c6c2cfee 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts @@ -23,6 +23,7 @@ import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadat import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; +import { ViewModule } from 'src/modules/view/view.module'; import { FieldMetadataEntity } from './field-metadata.entity'; import { FieldMetadataService } from './field-metadata.service'; @@ -45,6 +46,7 @@ import { UpdateFieldInput } from './dtos/update-field.input'; DataSourceModule, TypeORMModule, ActorModule, + ViewModule, ], services: [ IsFieldMetadataDefaultValue, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index 6ae115cd6..93f59d98a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -57,6 +57,7 @@ import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; +import { ViewService } from 'src/modules/view/services/view.service'; import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; import { isDefined } from 'src/utils/is-defined'; @@ -85,6 +86,7 @@ export class FieldMetadataService extends TypeOrmQueryService views.map((view) => view.id)); } + + async resetKanbanAggregateOperationByFieldMetadataId({ + workspaceId, + fieldMetadataId, + }: { + workspaceId: string; + fieldMetadataId: string; + }) { + const viewRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'view', + ); + + await viewRepository.update( + { kanbanAggregateOperationFieldMetadataId: fieldMetadataId }, + { + kanbanAggregateOperationFieldMetadataId: null, + kanbanAggregateOperation: AGGREGATE_OPERATIONS.count, + }, + ); + } } diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/delete-one-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/delete-one-field-metadata.integration-spec.ts new file mode 100644 index 000000000..ec5db3292 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/delete-one-field-metadata.integration-spec.ts @@ -0,0 +1,121 @@ +import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util'; +import { deleteOneOperationFactory } from 'test/integration/graphql/utils/delete-one-operation-factory.util'; +import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util'; +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { createCustomTextFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-custom-text-field-metadata.util'; +import { deleteOneFieldMetadataItemFactory } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata-factory.util'; +import { updateOneFieldMetadataFactory } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata-factory.util'; +import { createListingCustomObject } from 'test/integration/metadata/suites/object-metadata/utils/create-test-object-metadata.util'; +import { deleteOneObjectMetadataItem } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; + +describe('deleteOne', () => { + describe('Kanban aggregate operation', () => { + let listingObjectId = ''; + let testFieldId = ''; + let viewId = ''; + + beforeEach(async () => { + const { objectMetadataId: createdObjectId } = + await createListingCustomObject(); + + listingObjectId = createdObjectId; + const { fieldMetadataId: createdFieldMetadaId } = + await createCustomTextFieldMetadata(createdObjectId); + + testFieldId = createdFieldMetadaId; + + // create view + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'View', + gqlFields: ` + id + kanbanAggregateOperationFieldMetadataId + kanbanAggregateOperation + `, + data: { + kanbanAggregateOperationFieldMetadataId: testFieldId, + kanbanAggregateOperation: 'MAX', + objectMetadataId: listingObjectId, + name: 'By Type', + type: 'kanban', + icon: 'IconLayoutKanban', + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdView = response.body.data.createView; + + viewId = createdView.id; + }); + afterEach(async () => { + // delete view + const deleteViewOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'View', + gqlFields: 'id', + recordId: viewId, + }); + + await makeGraphqlAPIRequest(deleteViewOperation); + await deleteOneObjectMetadataItem(listingObjectId); + }); + it('should reset kanban aggregate operation when deleting a field used as kanbanAggregateOperationFieldMetadataId', async () => { + // Arrange + // 1. Check that view has expcted kanbanAggregateOperationFieldMetadataId and kanbanAggregateOperation + const findViewOperation = findOneOperationFactory({ + objectMetadataSingularName: 'view', + gqlFields: ` + id + kanbanAggregateOperationFieldMetadataId + kanbanAggregateOperation + `, + filter: { + id: { + eq: viewId, + }, + }, + }); + + const viewResponse = await makeGraphqlAPIRequest(findViewOperation); + + expect( + viewResponse.body.data.view.kanbanAggregateOperationFieldMetadataId, + ).toBe(testFieldId); + expect(viewResponse.body.data.view.kanbanAggregateOperation).toBe('MAX'); + + // Deactivate field to be able to delete it after + const deactivateFieldOperation = updateOneFieldMetadataFactory({ + input: { id: testFieldId, update: { isActive: false } }, + gqlFields: ` + id + isActive + `, + }); + + await makeMetadataAPIRequest(deactivateFieldOperation); + + // Act + const graphqlOperation = deleteOneFieldMetadataItemFactory({ + idToDelete: testFieldId, + }); + const response = await makeMetadataAPIRequest(graphqlOperation); + + // Assert + // 1. Field is deleted + expect(response.body.data.deleteOneField.id).toBe(testFieldId); + + // 2. Kanban aggregate operation has been reset on view using this field as kanbanAggregateOperationFieldMetadataId + const updatedViewResponse = + await makeGraphqlAPIRequest(findViewOperation); + + expect( + updatedViewResponse.body.data.view + .kanbanAggregateOperationFieldMetadataId, + ).toBeNull(); + expect(updatedViewResponse.body.data.view.kanbanAggregateOperation).toBe( + 'COUNT', + ); + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata.integration-spec.ts index 2989f8ea4..fc20c7907 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata.integration-spec.ts @@ -1,4 +1,4 @@ -import { createTestTextFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-test-field-metadata.util'; +import { createCustomTextFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-custom-text-field-metadata.util'; import { deleteFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util'; import { updateOneFieldMetadataFactory } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata-factory.util'; import { createListingCustomObject } from 'test/integration/metadata/suites/object-metadata/utils/create-test-object-metadata.util'; @@ -17,7 +17,7 @@ describe('updateOne', () => { listingObjectId = createdObjectId; const { fieldMetadataId: createdFieldMetadaId } = - await createTestTextFieldMetadata(createdObjectId); + await createCustomTextFieldMetadata(createdObjectId); testFieldId = createdFieldMetadaId; }); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-test-field-metadata.util.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-custom-text-field-metadata.util.ts similarity index 94% rename from packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-test-field-metadata.util.ts rename to packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-custom-text-field-metadata.util.ts index a1d932ee0..0544b4935 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-test-field-metadata.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-custom-text-field-metadata.util.ts @@ -4,7 +4,7 @@ import { FieldMetadataType } from 'twenty-shared'; const FIELD_NAME = 'testName'; -export const createTestTextFieldMetadata = async ( +export const createCustomTextFieldMetadata = async ( objectMetadataItemId: string, ) => { const createFieldInput = {