mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-31 04:37:56 +00:00 
			
		
		
		
	fix: when field metadata SELECT type is edited update view groups (#8344)
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
		| @@ -21,6 +21,7 @@ const baseFields = ` | |||||||
|   settings |   settings | ||||||
| `; | `; | ||||||
|  |  | ||||||
|  |  | ||||||
| export const queries = { | export const queries = { | ||||||
|   deleteMetadataField: gql` |   deleteMetadataField: gql` | ||||||
|     mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) { |     mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) { | ||||||
| @@ -29,6 +30,37 @@ export const queries = { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   `, |   `, | ||||||
|  |   findManyViewsQuery: gql` | ||||||
|  |     query FindManyViews($filter: ViewFilterInput, $orderBy: [ViewOrderByInput], $lastCursor: String, $limit: Int) { | ||||||
|  |         views(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor) { | ||||||
|  |           edges { | ||||||
|  |             node { | ||||||
|  |               __typename | ||||||
|  |               id | ||||||
|  |               viewGroups { | ||||||
|  |                 edges { | ||||||
|  |                   node { | ||||||
|  |                     __typename | ||||||
|  |                     fieldMetadataId | ||||||
|  |                     fieldValue | ||||||
|  |                     id | ||||||
|  |                     isVisible | ||||||
|  |                     position | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |             cursor | ||||||
|  |           } | ||||||
|  |           pageInfo { | ||||||
|  |             hasNextPage | ||||||
|  |             hasPreviousPage | ||||||
|  |             startCursor | ||||||
|  |             endCursor | ||||||
|  |           } | ||||||
|  |           totalCount | ||||||
|  |         } | ||||||
|  |       }`, | ||||||
|   deleteMetadataFieldRelation: gql` |   deleteMetadataFieldRelation: gql` | ||||||
|     mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) { |     mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) { | ||||||
|       deleteOneRelation(input: { id: $idToDelete }) { |       deleteOneRelation(input: { id: $idToDelete }) { | ||||||
|   | |||||||
| @@ -1,12 +1,11 @@ | |||||||
| import { MockedProvider } from '@apollo/client/testing'; |  | ||||||
| import { renderHook } from '@testing-library/react'; | import { renderHook } from '@testing-library/react'; | ||||||
| import { act, ReactNode } from 'react'; | import { act } from 'react'; | ||||||
| import { RecoilRoot } from 'recoil'; |  | ||||||
|  |  | ||||||
| import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; | import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; | ||||||
| import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; | import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; | ||||||
| import { FieldMetadataType, RelationDefinitionType } from '~/generated/graphql'; | import { FieldMetadataType, RelationDefinitionType } from '~/generated/graphql'; | ||||||
|  |  | ||||||
|  | import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; | ||||||
| import { | import { | ||||||
|   FIELD_METADATA_ID, |   FIELD_METADATA_ID, | ||||||
|   FIELD_RELATION_METADATA_ID, |   FIELD_RELATION_METADATA_ID, | ||||||
| @@ -58,6 +57,31 @@ const fieldRelationMetadataItem: FieldMetadataItem = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const mocks = [ | const mocks = [ | ||||||
|  |   { | ||||||
|  |     request: { | ||||||
|  |       query: queries.findManyViewsQuery, | ||||||
|  |       variables: { | ||||||
|  |         filter: { | ||||||
|  |           objectMetadataId: { eq: '25611fce-6637-4089-b0ca-91afeec95784' }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     result: jest.fn(() => ({ | ||||||
|  |       data: { | ||||||
|  |         views: { | ||||||
|  |           __typename: 'ViewConnection', | ||||||
|  |           totalCount: 0, | ||||||
|  |           pageInfo: { | ||||||
|  |             __typename: 'PageInfo', | ||||||
|  |             hasNextPage: false, | ||||||
|  |             startCursor: '', | ||||||
|  |             endCursor: '', | ||||||
|  |           }, | ||||||
|  |           edges: [], | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     })), | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     request: { |     request: { | ||||||
|       query: queries.deleteMetadataField, |       query: queries.deleteMetadataField, | ||||||
| @@ -115,13 +139,9 @@ const mocks = [ | |||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| const Wrapper = ({ children }: { children: ReactNode }) => ( | const Wrapper = getJestMetadataAndApolloMocksWrapper({ | ||||||
|   <RecoilRoot> |   apolloMocks: mocks, | ||||||
|     <MockedProvider mocks={mocks} addTypename={false}> | }); | ||||||
|       {children} |  | ||||||
|     </MockedProvider> |  | ||||||
|   </RecoilRoot> |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| describe('useFieldMetadataItem', () => { | describe('useFieldMetadataItem', () => { | ||||||
|   it('should activateMetadataField', async () => { |   it('should activateMetadataField', async () => { | ||||||
| @@ -130,7 +150,10 @@ describe('useFieldMetadataItem', () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     await act(async () => { |     await act(async () => { | ||||||
|       const res = await result.current.activateMetadataField(fieldMetadataItem); |       const res = await result.current.activateMetadataField( | ||||||
|  |         fieldMetadataItem.id, | ||||||
|  |         objectMetadataId, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       expect(res.data).toEqual({ |       expect(res.data).toEqual({ | ||||||
|         updateOneField: responseData.default, |         updateOneField: responseData.default, | ||||||
| @@ -162,8 +185,10 @@ describe('useFieldMetadataItem', () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     await act(async () => { |     await act(async () => { | ||||||
|       const res = |       const res = await result.current.deactivateMetadataField( | ||||||
|         await result.current.deactivateMetadataField(fieldMetadataItem); |         fieldMetadataItem.id, | ||||||
|  |         objectMetadataId, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       expect(res.data).toEqual({ |       expect(res.data).toEqual({ | ||||||
|         updateOneField: responseData.default, |         updateOneField: responseData.default, | ||||||
|   | |||||||
| @@ -40,15 +40,23 @@ export const useFieldMetadataItem = () => { | |||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const activateMetadataField = (metadataField: FieldMetadataItem) => |   const activateMetadataField = ( | ||||||
|  |     fieldMetadataId: string, | ||||||
|  |     objectMetadataId: string, | ||||||
|  |   ) => | ||||||
|     updateOneFieldMetadataItem({ |     updateOneFieldMetadataItem({ | ||||||
|       fieldMetadataIdToUpdate: metadataField.id, |       objectMetadataId: objectMetadataId, | ||||||
|  |       fieldMetadataIdToUpdate: fieldMetadataId, | ||||||
|       updatePayload: { isActive: true }, |       updatePayload: { isActive: true }, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   const deactivateMetadataField = (metadataField: FieldMetadataItem) => |   const deactivateMetadataField = ( | ||||||
|  |     fieldMetadataId: string, | ||||||
|  |     objectMetadataId: string, | ||||||
|  |   ) => | ||||||
|     updateOneFieldMetadataItem({ |     updateOneFieldMetadataItem({ | ||||||
|       fieldMetadataIdToUpdate: metadataField.id, |       objectMetadataId: objectMetadataId, | ||||||
|  |       fieldMetadataIdToUpdate: fieldMetadataId, | ||||||
|       updatePayload: { isActive: false }, |       updatePayload: { isActive: false }, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { useMutation } from '@apollo/client'; | import { useApolloClient, useMutation } from '@apollo/client'; | ||||||
| import { getOperationName } from '@apollo/client/utilities'; | import { getOperationName } from '@apollo/client/utilities'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
| @@ -9,10 +9,27 @@ import { | |||||||
| import { UPDATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations'; | import { UPDATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations'; | ||||||
| import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries'; | import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries'; | ||||||
|  |  | ||||||
|  | import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; | ||||||
|  | import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; | ||||||
| import { useApolloMetadataClient } from './useApolloMetadataClient'; | import { useApolloMetadataClient } from './useApolloMetadataClient'; | ||||||
|  |  | ||||||
| export const useUpdateOneFieldMetadataItem = () => { | export const useUpdateOneFieldMetadataItem = () => { | ||||||
|   const apolloMetadataClient = useApolloMetadataClient(); |   const apolloMetadataClient = useApolloMetadataClient(); | ||||||
|  |   const apolloClient = useApolloClient(); | ||||||
|  |  | ||||||
|  |   const { findManyRecordsQuery } = useFindManyRecordsQuery({ | ||||||
|  |     objectNameSingular: CoreObjectNameSingular.View, | ||||||
|  |     recordGqlFields: { | ||||||
|  |       id: true, | ||||||
|  |       viewGroups: { | ||||||
|  |         id: true, | ||||||
|  |         fieldMetadataId: true, | ||||||
|  |         isVisible: true, | ||||||
|  |         fieldValue: true, | ||||||
|  |         position: true, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   const [mutate] = useMutation< |   const [mutate] = useMutation< | ||||||
|     UpdateOneFieldMetadataItemMutation, |     UpdateOneFieldMetadataItemMutation, | ||||||
| @@ -22,9 +39,11 @@ export const useUpdateOneFieldMetadataItem = () => { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   const updateOneFieldMetadataItem = async ({ |   const updateOneFieldMetadataItem = async ({ | ||||||
|  |     objectMetadataId, | ||||||
|     fieldMetadataIdToUpdate, |     fieldMetadataIdToUpdate, | ||||||
|     updatePayload, |     updatePayload, | ||||||
|   }: { |   }: { | ||||||
|  |     objectMetadataId: string; | ||||||
|     fieldMetadataIdToUpdate: UpdateOneFieldMetadataItemMutationVariables['idToUpdate']; |     fieldMetadataIdToUpdate: UpdateOneFieldMetadataItemMutationVariables['idToUpdate']; | ||||||
|     updatePayload: Pick< |     updatePayload: Pick< | ||||||
|       UpdateOneFieldMetadataItemMutationVariables['updatePayload'], |       UpdateOneFieldMetadataItemMutationVariables['updatePayload'], | ||||||
| @@ -37,7 +56,7 @@ export const useUpdateOneFieldMetadataItem = () => { | |||||||
|       | 'options' |       | 'options' | ||||||
|     >; |     >; | ||||||
|   }) => { |   }) => { | ||||||
|     return await mutate({ |     const result = await mutate({ | ||||||
|       variables: { |       variables: { | ||||||
|         idToUpdate: fieldMetadataIdToUpdate, |         idToUpdate: fieldMetadataIdToUpdate, | ||||||
|         updatePayload: { |         updatePayload: { | ||||||
| @@ -48,6 +67,20 @@ export const useUpdateOneFieldMetadataItem = () => { | |||||||
|       awaitRefetchQueries: true, |       awaitRefetchQueries: true, | ||||||
|       refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''], |       refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''], | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     await apolloClient.query({ | ||||||
|  |       query: findManyRecordsQuery, | ||||||
|  |       variables: { | ||||||
|  |         filter: { | ||||||
|  |           objectMetadataId: { | ||||||
|  |             eq: objectMetadataId, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       fetchPolicy: 'network-only', | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return result; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import { useRecoilCallback } from 'recoil'; | import { useRecoilCallback } from 'recoil'; | ||||||
|  |  | ||||||
| import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; | import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; | ||||||
| import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; |  | ||||||
| import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; | import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; | ||||||
|  | import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; | ||||||
|  |  | ||||||
| export const useSetRecordBoardColumns = (recordBoardId?: string) => { | export const useSetRecordBoardColumns = (recordBoardId?: string) => { | ||||||
|   const { scopeId, columnIdsState, columnsFamilySelector } = |   const { scopeId, columnIdsState, columnsFamilySelector } = | ||||||
| @@ -19,11 +19,9 @@ export const useSetRecordBoardColumns = (recordBoardId?: string) => { | |||||||
|           .filter(({ isVisible }) => isVisible) |           .filter(({ isVisible }) => isVisible) | ||||||
|           .map(({ id }) => id); |           .map(({ id }) => id); | ||||||
|  |  | ||||||
|         if (isDeeplyEqual(currentColumnsIds, columnIds)) { |         if (!isDeeplyEqual(currentColumnsIds, columnIds)) { | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|           set(columnIdsState, columnIds); |           set(columnIdsState, columnIds); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         columns.forEach((column) => { |         columns.forEach((column) => { | ||||||
|           const currentColumn = snapshot |           const currentColumn = snapshot | ||||||
|   | |||||||
| @@ -127,7 +127,10 @@ export const SettingsObjectFieldItemTableRow = ({ | |||||||
|   const handleDisableField = async ( |   const handleDisableField = async ( | ||||||
|     activeFieldMetadatItem: FieldMetadataItem, |     activeFieldMetadatItem: FieldMetadataItem, | ||||||
|   ) => { |   ) => { | ||||||
|     await deactivateMetadataField(activeFieldMetadatItem); |     await deactivateMetadataField( | ||||||
|  |       activeFieldMetadatItem.id, | ||||||
|  |       objectMetadataItem.id, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     const deletedViewIds = allViews |     const deletedViewIds = allViews | ||||||
|       .map((view) => { |       .map((view) => { | ||||||
| @@ -272,7 +275,9 @@ export const SettingsObjectFieldItemTableRow = ({ | |||||||
|             isCustomField={fieldMetadataItem.isCustom === true} |             isCustomField={fieldMetadataItem.isCustom === true} | ||||||
|             scopeKey={fieldMetadataItem.id} |             scopeKey={fieldMetadataItem.id} | ||||||
|             onEdit={() => navigate(linkToNavigate)} |             onEdit={() => navigate(linkToNavigate)} | ||||||
|             onActivate={() => activateMetadataField(fieldMetadataItem)} |             onActivate={() => | ||||||
|  |               activateMetadataField(fieldMetadataItem.id, objectMetadataItem.id) | ||||||
|  |             } | ||||||
|             onDelete={() => deleteMetadataField(fieldMetadataItem)} |             onDelete={() => deleteMetadataField(fieldMetadataItem)} | ||||||
|           /> |           /> | ||||||
|         ) : ( |         ) : ( | ||||||
|   | |||||||
| @@ -137,6 +137,7 @@ export const SettingsObjectFieldEdit = () => { | |||||||
|  |  | ||||||
|         if (isDefined(relationFieldMetadataItem)) { |         if (isDefined(relationFieldMetadataItem)) { | ||||||
|           await updateOneFieldMetadataItem({ |           await updateOneFieldMetadataItem({ | ||||||
|  |             objectMetadataId: objectMetadataItem.id, | ||||||
|             fieldMetadataIdToUpdate: relationFieldMetadataItem.id, |             fieldMetadataIdToUpdate: relationFieldMetadataItem.id, | ||||||
|             updatePayload: formValues.relation.field, |             updatePayload: formValues.relation.field, | ||||||
|           }); |           }); | ||||||
| @@ -152,6 +153,7 @@ export const SettingsObjectFieldEdit = () => { | |||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         await updateOneFieldMetadataItem({ |         await updateOneFieldMetadataItem({ | ||||||
|  |           objectMetadataId: objectMetadataItem.id, | ||||||
|           fieldMetadataIdToUpdate: fieldMetadataItem.id, |           fieldMetadataIdToUpdate: fieldMetadataItem.id, | ||||||
|           updatePayload: formattedInput, |           updatePayload: formattedInput, | ||||||
|         }); |         }); | ||||||
| @@ -168,12 +170,12 @@ export const SettingsObjectFieldEdit = () => { | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const handleDeactivate = async () => { |   const handleDeactivate = async () => { | ||||||
|     await deactivateMetadataField(fieldMetadataItem); |     await deactivateMetadataField(fieldMetadataItem.id, objectMetadataItem.id); | ||||||
|     navigate(`/settings/objects/${objectSlug}`); |     navigate(`/settings/objects/${objectSlug}`); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const handleActivate = async () => { |   const handleActivate = async () => { | ||||||
|     await activateMetadataField(fieldMetadataItem); |     await activateMetadataField(fieldMetadataItem.id, objectMetadataItem.id); | ||||||
|     navigate(`/settings/objects/${objectSlug}`); |     navigate(`/settings/objects/${objectSlug}`); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dto | |||||||
| import { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/field-metadata-validation.service'; | import { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/field-metadata-validation.service'; | ||||||
| import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver'; | import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver'; | ||||||
| import { FieldMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/field-metadata/interceptors/field-metadata-graphql-api-exception.interceptor'; | import { FieldMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/field-metadata/interceptors/field-metadata-graphql-api-exception.interceptor'; | ||||||
|  | import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service'; | ||||||
| import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator'; | import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator'; | ||||||
| import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator'; | import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator'; | ||||||
| import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; | import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; | ||||||
| @@ -48,6 +49,7 @@ import { UpdateFieldInput } from './dtos/update-field.input'; | |||||||
|       services: [ |       services: [ | ||||||
|         IsFieldMetadataDefaultValue, |         IsFieldMetadataDefaultValue, | ||||||
|         FieldMetadataService, |         FieldMetadataService, | ||||||
|  |         FieldMetadataRelatedRecordsService, | ||||||
|         FieldMetadataValidationService, |         FieldMetadataValidationService, | ||||||
|       ], |       ], | ||||||
|       resolvers: [ |       resolvers: [ | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ import { | |||||||
|   FieldMetadataException, |   FieldMetadataException, | ||||||
|   FieldMetadataExceptionCode, |   FieldMetadataExceptionCode, | ||||||
| } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; | } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; | ||||||
|  | import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service'; | ||||||
| import { assertDoesNotNullifyDefaultValueForNonNullableField } from 'src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util'; | import { assertDoesNotNullifyDefaultValueForNonNullableField } from 'src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util'; | ||||||
| import { | import { | ||||||
|   computeColumnName, |   computeColumnName, | ||||||
| @@ -28,6 +29,7 @@ import { | |||||||
| } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; | } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; | ||||||
| import { generateNullable } from 'src/engine/metadata-modules/field-metadata/utils/generate-nullable'; | import { generateNullable } from 'src/engine/metadata-modules/field-metadata/utils/generate-nullable'; | ||||||
| import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; | import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; | ||||||
|  | import { isSelectFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util'; | ||||||
| import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; | import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; | ||||||
| import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; | import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; | ||||||
| import { | import { | ||||||
| @@ -83,6 +85,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit | |||||||
|     private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, |     private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, | ||||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|     private readonly fieldMetadataValidationService: FieldMetadataValidationService, |     private readonly fieldMetadataValidationService: FieldMetadataValidationService, | ||||||
|  |     private readonly fieldMetadataRelatedRecordsService: FieldMetadataRelatedRecordsService, | ||||||
|   ) { |   ) { | ||||||
|     super(fieldMetadataRepository); |     super(fieldMetadataRepository); | ||||||
|   } |   } | ||||||
| @@ -418,6 +421,16 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit | |||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       if ( | ||||||
|  |         updatedFieldMetadata.isActive && | ||||||
|  |         isSelectFieldMetadataType(updatedFieldMetadata.type) | ||||||
|  |       ) { | ||||||
|  |         await this.fieldMetadataRelatedRecordsService.updateRelatedViewGroups( | ||||||
|  |           existingFieldMetadata, | ||||||
|  |           updatedFieldMetadata, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       if ( |       if ( | ||||||
|         fieldMetadataInput.name || |         fieldMetadataInput.name || | ||||||
|         updatableFieldInput.options || |         updatableFieldInput.options || | ||||||
|   | |||||||
| @@ -0,0 +1,151 @@ | |||||||
|  | import { Injectable } from '@nestjs/common'; | ||||||
|  |  | ||||||
|  | import { In } from 'typeorm'; | ||||||
|  |  | ||||||
|  | import { | ||||||
|  |   FieldMetadataComplexOption, | ||||||
|  |   FieldMetadataDefaultOption, | ||||||
|  | } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; | ||||||
|  | import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; | ||||||
|  | import { isSelectFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util'; | ||||||
|  | import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||||
|  | import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity'; | ||||||
|  | import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; | ||||||
|  |  | ||||||
|  | type Differences<T> = { | ||||||
|  |   created: T[]; | ||||||
|  |   updated: { old: T; new: T }[]; | ||||||
|  |   deleted: T[]; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @Injectable() | ||||||
|  | export class FieldMetadataRelatedRecordsService { | ||||||
|  |   constructor( | ||||||
|  |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|  |   ) {} | ||||||
|  |  | ||||||
|  |   public async updateRelatedViewGroups( | ||||||
|  |     oldFieldMetadata: FieldMetadataEntity, | ||||||
|  |     newFieldMetadata: FieldMetadataEntity, | ||||||
|  |   ) { | ||||||
|  |     if ( | ||||||
|  |       !isSelectFieldMetadataType(newFieldMetadata.type) || | ||||||
|  |       !isSelectFieldMetadataType(oldFieldMetadata.type) | ||||||
|  |     ) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const views = await this.getFieldMetadataViews(newFieldMetadata); | ||||||
|  |  | ||||||
|  |     const { created, updated, deleted } = this.getOptionsDifferences( | ||||||
|  |       oldFieldMetadata.options, | ||||||
|  |       newFieldMetadata.options, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const viewGroupRepository = | ||||||
|  |       await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewGroupWorkspaceEntity>( | ||||||
|  |         newFieldMetadata.workspaceId, | ||||||
|  |         'viewGroup', | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |     for (const view of views) { | ||||||
|  |       const maxPosition = view.viewGroups.reduce( | ||||||
|  |         (max, viewGroup) => Math.max(max, viewGroup.position), | ||||||
|  |         0, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       const viewGroupsToCreate = created.map((option, index) => | ||||||
|  |         viewGroupRepository.create({ | ||||||
|  |           fieldMetadataId: newFieldMetadata.id, | ||||||
|  |           fieldValue: option.value, | ||||||
|  |           position: maxPosition + index, | ||||||
|  |           isVisible: true, | ||||||
|  |           viewId: view.id, | ||||||
|  |         }), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       await viewGroupRepository.insert(viewGroupsToCreate); | ||||||
|  |  | ||||||
|  |       for (const { old: oldOption, new: newOption } of updated) { | ||||||
|  |         const viewGroup = view.viewGroups.find( | ||||||
|  |           (viewGroup) => viewGroup.fieldValue === oldOption.value, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         if (!viewGroup) { | ||||||
|  |           throw new Error(`View group not found for option ${oldOption.value}`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await viewGroupRepository.update( | ||||||
|  |           { | ||||||
|  |             id: viewGroup.id, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             fieldValue: newOption.value, | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const valuesToDelete = deleted.map((option) => option.value); | ||||||
|  |  | ||||||
|  |       await viewGroupRepository.delete({ | ||||||
|  |         fieldMetadataId: newFieldMetadata.id, | ||||||
|  |         fieldValue: In(valuesToDelete), | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private getOptionsDifferences( | ||||||
|  |     oldOptions: (FieldMetadataDefaultOption | FieldMetadataComplexOption)[], | ||||||
|  |     newOptions: (FieldMetadataDefaultOption | FieldMetadataComplexOption)[], | ||||||
|  |   ) { | ||||||
|  |     const differences: Differences< | ||||||
|  |       FieldMetadataDefaultOption | FieldMetadataComplexOption | ||||||
|  |     > = { | ||||||
|  |       created: [], | ||||||
|  |       updated: [], | ||||||
|  |       deleted: [], | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const oldOptionsMap = new Map( | ||||||
|  |       oldOptions.map((option) => [option.id, option]), | ||||||
|  |     ); | ||||||
|  |     const newOptionsMap = new Map( | ||||||
|  |       newOptions.map((option) => [option.id, option]), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     for (const newOption of newOptions) { | ||||||
|  |       const oldOption = oldOptionsMap.get(newOption.id); | ||||||
|  |  | ||||||
|  |       if (!oldOption) { | ||||||
|  |         differences.created.push(newOption); | ||||||
|  |       } else if (oldOption.value !== newOption.value) { | ||||||
|  |         differences.updated.push({ old: oldOption, new: newOption }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (const oldOption of oldOptions) { | ||||||
|  |       if (!newOptionsMap.has(oldOption.id)) { | ||||||
|  |         differences.deleted.push(oldOption); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return differences; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async getFieldMetadataViews( | ||||||
|  |     fieldMetadata: FieldMetadataEntity, | ||||||
|  |   ): Promise<ViewWorkspaceEntity[]> { | ||||||
|  |     const viewRepository = | ||||||
|  |       await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>( | ||||||
|  |         fieldMetadata.workspaceId, | ||||||
|  |         'view', | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |     return await viewRepository.find({ | ||||||
|  |       where: { | ||||||
|  |         kanbanFieldMetadataId: fieldMetadata.id, | ||||||
|  |       }, | ||||||
|  |       relations: ['viewGroups'], | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; | ||||||
|  |  | ||||||
|  | export const isSelectFieldMetadataType = ( | ||||||
|  |   type: FieldMetadataType, | ||||||
|  | ): type is FieldMetadataType.SELECT => { | ||||||
|  |   return type === FieldMetadataType.SELECT; | ||||||
|  | }; | ||||||
		Reference in New Issue
	
	Block a user
	 Jérémy M
					Jérémy M