From 8ab01ebef44d9efe6bd46748e72f744b8da984ed Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:32:57 +0100 Subject: [PATCH] [BUG] Record settings not saved (#9762) # Introduction By initially fixing this Fixes #9381, discovered other behavior that have been fix. Overall we encountered a bug that corrupts a workspace and make the browser + api crash This issue https://github.com/twentyhq/core-team-issues/issues/25 suggests a refactor that has final save button instead of auto-save ## `labelIdentifierFieldMetadataId` form default value The default value resulted in being undefined, resulting in react hook form `labelIdentifierFieldMetadataId` is required field error. ### Fix Setting default value fallback to `null` as field is `nullable` ## `SettingsDataModelObjectSettingsFormCard` never triggers form Unless I'm mistaken in production touching any fields within `SettingsDataModelObjectSettingsFormCard` would never trigger form submission until you also modify `SettingsDataModelObjectAboutForm` fields ### Fix Provide and apply `onblur` that triggers the form on both `SettingsDataModelObjectSettingsFormCard` inputs ## Wrong default `labelIdentifierFieldMetadataItem` on first page render When landing on the page for the first time, if a custom `labelIdentifierFieldMetadataItem` has been set it won't be computed within the `PreviewCard`. Occurs when `labelIdentifierFieldMetadataId` form default value is undefined, due to `any` injection. ### Fix In the `getLabelIdentifierFieldMetadataItem` check the `labelIdentifierFieldMetadataIdFormValue` definition, if undefined fallback to current `objectMetadata` identifier --------- Co-authored-by: Charles Bochet --- .../__tests__/useSignInWithGoogle.test.ts | 1 + .../__tests__/useSignInWithMicrosoft.test.ts | 1 + .../useCreateOneObjectMetadataItem.ts | 2 +- .../useDeleteOneObjectMetadataItem.ts | 2 +- .../useFilteredObjectMetadataItems.ts | 2 +- .../types/ObjectMetadataItem.ts | 7 ++- .../__tests__/isLabelIdentifierField.test.ts | 16 ++++- ...bjectMetadataItemsToObjectMetadataItems.ts | 30 ++++++---- .../objectMetadataItemSchema.test.ts | 44 +++++++++++++- .../objectMetadataItemSchema.ts | 2 +- .../useAggregateRecordsQuery.test.tsx | 5 +- .../__tests__/turnSortsIntoOrderBy.test.ts | 1 + ...ildRecordGqlFieldsAggregateForView.test.ts | 2 +- .../useLimitPerMetadataItem.test.tsx | 1 + .../__tests__/generateAggregateQuery.test.ts | 2 + .../preview/hooks/useFieldPreviewValue.ts | 1 + .../components/tabs/ObjectSettings.tsx | 5 +- .../SettingsDataModelObjectAboutForm.tsx | 1 + ...SettingsDataModelObjectIdentifiersForm.tsx | 60 +++++++++---------- ...ettingsDataModelObjectSettingsFormCard.tsx | 34 ++++------- .../generated/mock-metadata-query-result.ts | 2 +- .../generatedMockObjectMetadataItems.ts | 29 +++++---- 22 files changed, 161 insertions(+), 89 deletions(-) diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithGoogle.test.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithGoogle.test.ts index 865472c45..90dde88b0 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithGoogle.test.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithGoogle.test.ts @@ -8,6 +8,7 @@ import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMeta jest.mock('react-router-dom', () => ({ useParams: jest.fn(), useSearchParams: jest.fn(), + Link: jest.fn(), })); jest.mock('@/auth/hooks/useAuth', () => ({ diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithMicrosoft.test.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithMicrosoft.test.ts index e4a5b1b2c..577ee7202 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithMicrosoft.test.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSignInWithMicrosoft.test.ts @@ -7,6 +7,7 @@ import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMeta jest.mock('react-router-dom', () => ({ useParams: jest.fn(), useSearchParams: jest.fn(), + Link: jest.fn(), })); jest.mock('@/auth/hooks/useAuth', () => ({ diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts index ce4d5cfbe..170d396b9 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts @@ -81,6 +81,6 @@ export const responseData = { isActive: true, createdAt: '', updatedAt: '', - labelIdentifierFieldMetadataId: '', + labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1', imageIdentifierFieldMetadataId: '', }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useDeleteOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useDeleteOneObjectMetadataItem.ts index e7be9105b..a580e6e4f 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useDeleteOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useDeleteOneObjectMetadataItem.ts @@ -36,6 +36,6 @@ export const responseData = { isActive: true, createdAt: '', updatedAt: '', - labelIdentifierFieldMetadataId: '', + labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1', imageIdentifierFieldMetadataId: '', }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFilteredObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFilteredObjectMetadataItems.ts index a2419898a..8e5ba1ea3 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFilteredObjectMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFilteredObjectMetadataItems.ts @@ -50,6 +50,6 @@ export const responseData = { isActive: true, createdAt: '', updatedAt: '', - labelIdentifierFieldMetadataId: '', + labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1', imageIdentifierFieldMetadataId: '', }; diff --git a/packages/twenty-front/src/modules/object-metadata/types/ObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/types/ObjectMetadataItem.ts index 61c0fc495..09c23eeb8 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/ObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/ObjectMetadataItem.ts @@ -5,9 +5,14 @@ import { FieldMetadataItem } from './FieldMetadataItem'; export type ObjectMetadataItem = Omit< GeneratedObject, - '__typename' | 'fields' | 'dataSourceId' | 'indexMetadatas' + | '__typename' + | 'fields' + | 'dataSourceId' + | 'indexMetadatas' + | 'labelIdentifierFieldMetadataId' > & { __typename?: string; fields: FieldMetadataItem[]; + labelIdentifierFieldMetadataId: string; indexMetadatas: IndexMetadataItem[]; }; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/isLabelIdentifierField.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/isLabelIdentifierField.test.ts index 85ad1f0eb..6cd5dd8c1 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/isLabelIdentifierField.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/isLabelIdentifierField.test.ts @@ -1,11 +1,23 @@ import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; describe('isLabelIdentifierField', () => { - it('should work as expected', () => { + it('should not find unknown labelIdentifier', () => { const res = isLabelIdentifierField({ fieldMetadataItem: { id: 'fieldId', name: 'fieldName' }, - objectMetadataItem: {}, + objectMetadataItem: { + labelIdentifierFieldMetadataId: 'unknown', + }, }); expect(res).toBe(false); }); + + it('should find known labelIdentifier', () => { + const res = isLabelIdentifierField({ + fieldMetadataItem: { id: 'fieldId', name: 'fieldName' }, + objectMetadataItem: { + labelIdentifierFieldMetadataId: 'fieldId', + }, + }); + expect(res).toBe(true); + }); }); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts index 0ab2a8229..50c6d0031 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts @@ -1,5 +1,5 @@ +import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql'; - import { ObjectMetadataItem } from '../types/ObjectMetadataItem'; export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({ @@ -8,16 +8,24 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({ pagedObjectMetadataItems: ObjectMetadataItemsQuery | undefined; }) => { const formattedObjects: ObjectMetadataItem[] = - pagedObjectMetadataItems?.objects.edges.map((object) => ({ - ...object.node, - fields: object.node.fields.edges.map((field) => field.node), - indexMetadatas: object.node.indexMetadatas?.edges.map((index) => ({ - ...index.node, - indexFieldMetadatas: index.node.indexFieldMetadatas?.edges.map( - (indexField) => indexField.node, - ), - })), - })) ?? []; + pagedObjectMetadataItems?.objects.edges.map((object) => { + const labelIdentifierFieldMetadataId = + objectMetadataItemSchema.shape.labelIdentifierFieldMetadataId.parse( + object.node.labelIdentifierFieldMetadataId, + ); + + return { + ...object.node, + fields: object.node.fields.edges.map((field) => field.node), + labelIdentifierFieldMetadataId, + indexMetadatas: object.node.indexMetadatas?.edges.map((index) => ({ + ...index.node, + indexFieldMetadatas: index.node.indexFieldMetadatas?.edges.map( + (indexField) => indexField.node, + ), + })), + }; + }) ?? []; return formattedObjects; }; diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts index 64e513214..8697d279a 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts @@ -1,3 +1,4 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { objectMetadataItemSchema } from '../objectMetadataItemSchema'; @@ -15,16 +16,37 @@ describe('objectMetadataItemSchema', () => { expect(result).toEqual(validObjectMetadataItem); }); + it('fails for an invalid object metadata item that has null labelIdentifier', () => { + // Given + const validObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', + ); + expect(validObjectMetadataItem).not.toBeUndefined(); + if (validObjectMetadataItem === undefined) + throw new Error('Should never occurs'); + + // When + const result = objectMetadataItemSchema.safeParse({ + ...validObjectMetadataItem, + labelIdentifierFieldMetadataId: null, + }); + + // Then + expect(result.success).toEqual(false); + }); + it('fails for an invalid object metadata item', () => { // Given - const invalidObjectMetadataItem = { + const invalidObjectMetadataItem: Partial< + Record + > = { createdAt: 'invalid date', - dataSourceId: 'invalid uuid', fields: 'not an array', icon: 'invalid icon', isActive: 'not a boolean', isCustom: 'not a boolean', isSystem: 'not a boolean', + labelIdentifierFieldMetadataId: 'not a uuid', labelPlural: 123, labelSingular: 123, namePlural: 'notCamelCase', @@ -41,4 +63,22 @@ describe('objectMetadataItemSchema', () => { // Then expect(result.success).toBe(false); }); + + it('should fail to parse empty string as LabelIdentifier', () => { + const emptyString = ''; + const result = + objectMetadataItemSchema.shape.labelIdentifierFieldMetadataId.safeParse( + emptyString, + ); + expect(result.success).toBe(false); + }); + + it('should succeed to parse valid uuid as LabelIdentifier', () => { + const validUuid = '20202020-ae24-4871-b445-10cc8872cb10'; + const result = + objectMetadataItemSchema.shape.labelIdentifierFieldMetadataId.safeParse( + validUuid, + ); + expect(result.success).toBe(true); + }); }); diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts index 1c40e6258..84ed6083c 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts @@ -20,7 +20,7 @@ export const objectMetadataItemSchema = z.object({ isCustom: z.boolean(), isRemote: z.boolean(), isSystem: z.boolean(), - labelIdentifierFieldMetadataId: z.string().uuid().nullable(), + labelIdentifierFieldMetadataId: z.string().uuid(), labelPlural: metadataLabelSchema(), labelSingular: metadataLabelSchema(), namePlural: camelCaseStringSchema, diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx index d2d3faef8..7b94218aa 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx @@ -17,12 +17,13 @@ const mockObjectMetadataItem: ObjectMetadataItem = { labelSingular: 'Company', labelPlural: 'Companies', isCustom: false, + labelIdentifierFieldMetadataId: '20202020-dd4a-4ea4-bb7b-1c7300491b65', isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), fields: [ { - id: 'field-1', + id: '20202020-fed9-4ce5-9502-02a8efaf46e1', name: 'amount', label: 'Amount', type: FieldMetadataType.NUMBER, @@ -32,7 +33,7 @@ const mockObjectMetadataItem: ObjectMetadataItem = { updatedAt: new Date().toISOString(), } as FieldMetadataItem, { - id: 'field-2', + id: '20202020-dd4a-4ea4-bb7b-1c7300491b65', name: 'name', label: 'Name', type: FieldMetadataType.TEXT, diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts index 6d3d8b92e..4b0b58c2e 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts @@ -18,6 +18,7 @@ const objectMetadataItem: ObjectMetadataItem = { updatedAt: '2021-01-01', nameSingular: 'object1', namePlural: 'object1s', + labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1', icon: 'icon', isActive: true, isSystem: false, diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForView.test.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForView.test.ts index a078013e8..e79eb3a3a 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForView.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregateForView.test.ts @@ -18,7 +18,7 @@ describe('buildRecordGqlFieldsAggregateForView', () => { isActive: true, isSystem: false, isRemote: false, - labelIdentifierFieldMetadataId: null, + labelIdentifierFieldMetadataId: '06b33746-5293-4d07-9f7f-ebf5ad396064', imageIdentifierFieldMetadataId: null, isLabelSyncedWithName: true, fields: [ diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useLimitPerMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useLimitPerMetadataItem.test.tsx index 8596a3343..b28747d6c 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useLimitPerMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useLimitPerMetadataItem.test.tsx @@ -25,6 +25,7 @@ describe('useLimitPerMetadataItem', () => { labelSingular: 'labelSingular', namePlural: 'namePlural', nameSingular: 'nameSingular', + labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1', updatedAt: 'updatedAt', isLabelSyncedWithName: false, fields: [], diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts index 4ce449957..7cf2c38f8 100644 --- a/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts @@ -9,6 +9,7 @@ describe('generateAggregateQuery', () => { id: 'test-id', labelSingular: 'Company', labelPlural: 'Companies', + labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1', isCustom: false, isActive: true, createdAt: new Date().toISOString(), @@ -46,6 +47,7 @@ describe('generateAggregateQuery', () => { id: 'test-id', labelSingular: 'Person', labelPlural: 'People', + labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1', isCustom: false, isActive: true, createdAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts index d24e20f26..a2f1d9358 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts @@ -28,6 +28,7 @@ export const useFieldPreviewValue = ({ relationObjectMetadataItem: relationObjectMetadataItem ?? { fields: [], labelSingular: '', + labelIdentifierFieldMetadataId: '20202020-1000-4629-87e5-9a1fae1cc2fd', nameSingular: CoreObjectNameSingular.Company, }, skip: diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx index 6fcf55ad3..0d919f232 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx @@ -23,7 +23,6 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import styled from '@emotion/styled'; -import isEmpty from 'lodash.isempty'; import pick from 'lodash.pick'; import { useSetRecoilState } from 'recoil'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; @@ -70,6 +69,7 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { mode: 'onTouched', resolver: zodResolver(objectEditFormSchema), }); + const { isDirty } = formConfig.formState; const setNavigationMemorizedUrl = useSetRecoilState( navigationMemorizedUrlState, @@ -124,7 +124,7 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { const handleSave = async ( formValues: SettingsDataModelObjectEditFormValues, ) => { - if (isEmpty(formConfig.formState.dirtyFields) === true) { + if (!isDirty) { return; } try { @@ -202,6 +202,7 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { description="Choose the fields that will identify your records" /> formConfig.handleSubmit(handleSave)()} objectMetadataItem={objectMetadataItem} /> diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx index ce881e136..54233dc35 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx @@ -219,6 +219,7 @@ export const SettingsDataModelObjectAboutForm = ({ value={value ?? undefined} onChange={(nextValue) => onChange(nextValue ?? null)} disabled={disableEdition} + onBlur={onBlur} /> )} /> diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx index 29f5512e5..bafa6d387 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { IconCircleOff, isDefined, useIcons } from 'twenty-ui'; +import { IconCircleOff, useIcons } from 'twenty-ui'; import { z } from 'zod'; import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes'; @@ -19,11 +19,16 @@ export const settingsDataModelObjectIdentifiersFormSchema = export type SettingsDataModelObjectIdentifiersFormValues = z.infer< typeof settingsDataModelObjectIdentifiersFormSchema >; - +export type SettingsDataModelObjectIdentifiers = + keyof SettingsDataModelObjectIdentifiersFormValues; type SettingsDataModelObjectIdentifiersFormProps = { objectMetadataItem: ObjectMetadataItem; - defaultLabelIdentifierFieldMetadataId: string; + onBlur: () => void; }; +const LABEL_IDENTIFIER_FIELD_METADATA_ID: SettingsDataModelObjectIdentifiers = + 'labelIdentifierFieldMetadataId'; +const IMAGE_IDENTIFIER_FIELD_METADATA_ID: SettingsDataModelObjectIdentifiers = + 'imageIdentifierFieldMetadataId'; const StyledContainer = styled.div` display: flex; @@ -32,12 +37,11 @@ const StyledContainer = styled.div` export const SettingsDataModelObjectIdentifiersForm = ({ objectMetadataItem, - defaultLabelIdentifierFieldMetadataId, + onBlur, }: SettingsDataModelObjectIdentifiersFormProps) => { const { control } = useFormContext(); const { getIcon } = useIcons(); - const labelIdentifierFieldOptions = useMemo( () => getActiveFieldMetadataItems(objectMetadataItem) @@ -65,41 +69,37 @@ export const SettingsDataModelObjectIdentifiersForm = ({ {[ { label: 'Record label', - fieldName: 'labelIdentifierFieldMetadataId' as const, + fieldName: LABEL_IDENTIFIER_FIELD_METADATA_ID, options: labelIdentifierFieldOptions, + defaultValue: objectMetadataItem.labelIdentifierFieldMetadataId, }, { label: 'Record image', - fieldName: 'imageIdentifierFieldMetadataId' as const, + fieldName: IMAGE_IDENTIFIER_FIELD_METADATA_ID, options: imageIdentifierFieldOptions, + defaultValue: null, }, - ].map(({ fieldName, label, options }) => ( + ].map(({ fieldName, label, options, defaultValue }) => ( { - return ( - { + onChange(value); + onBlur(); + }} + /> + )} /> ))} diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard.tsx index 237196206..822e257e7 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard.tsx @@ -1,21 +1,18 @@ import styled from '@emotion/styled'; import { useMemo } from 'react'; -import { useFormContext } from 'react-hook-form'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; import { SettingsDataModelCardTitle } from '@/settings/data-model/components/SettingsDataModelCardTitle'; import { SettingsDataModelFieldPreviewCard } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; import { SettingsDataModelObjectSummary } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary'; -import { - SettingsDataModelObjectIdentifiersForm, - SettingsDataModelObjectIdentifiersFormValues, -} from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm'; +import { SettingsDataModelObjectIdentifiersForm } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm'; import { Trans } from '@lingui/react/macro'; import { Card, CardContent } from 'twenty-ui'; type SettingsDataModelObjectSettingsFormCardProps = { objectMetadataItem: ObjectMetadataItem; + onBlur: () => void; }; const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` @@ -38,22 +35,15 @@ const StyledObjectSummaryCardContent = styled(CardContent)` export const SettingsDataModelObjectSettingsFormCard = ({ objectMetadataItem, + onBlur, }: SettingsDataModelObjectSettingsFormCardProps) => { - const { watch: watchFormValue } = - useFormContext(); - - const labelIdentifierFieldMetadataIdFormValue = watchFormValue( - 'labelIdentifierFieldMetadataId', - ); - - const labelIdentifierFieldMetadataItem = useMemo( - () => - getLabelIdentifierFieldMetadataItem({ - fields: objectMetadataItem.fields, - labelIdentifierFieldMetadataId: labelIdentifierFieldMetadataIdFormValue, - }), - [labelIdentifierFieldMetadataIdFormValue, objectMetadataItem], - ); + const labelIdentifierFieldMetadataItem = useMemo(() => { + return getLabelIdentifierFieldMetadataItem({ + fields: objectMetadataItem.fields, + labelIdentifierFieldMetadataId: + objectMetadataItem.labelIdentifierFieldMetadataId, + }); + }, [objectMetadataItem]); return ( @@ -80,9 +70,7 @@ export const SettingsDataModelObjectSettingsFormCard = ({ diff --git a/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts b/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts index 31e08110e..18375a710 100644 --- a/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts +++ b/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts @@ -2868,7 +2868,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "isSystem": false, "createdAt": "2024-11-06T08:55:38.993Z", "updatedAt": "2024-11-06T08:55:38.993Z", - "labelIdentifierFieldMetadataId": null, + "labelIdentifierFieldMetadataId": "7896a006-eb14-481e-8197-661b7009a22e", "imageIdentifierFieldMetadataId": null, "shortcut": null, "isLabelSyncedWithName": false, diff --git a/packages/twenty-front/src/testing/mock-data/generatedMockObjectMetadataItems.ts b/packages/twenty-front/src/testing/mock-data/generatedMockObjectMetadataItems.ts index ea4a8fae3..665bc9c89 100644 --- a/packages/twenty-front/src/testing/mock-data/generatedMockObjectMetadataItems.ts +++ b/packages/twenty-front/src/testing/mock-data/generatedMockObjectMetadataItems.ts @@ -1,14 +1,23 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result'; export const generatedMockObjectMetadataItems: ObjectMetadataItem[] = - mockedStandardObjectMetadataQueryResult.objects.edges.map((edge) => ({ - ...edge.node, - fields: edge.node.fields.edges.map((edge) => edge.node), - indexMetadatas: edge.node.indexMetadatas.edges.map((index) => ({ - ...index.node, - indexFieldMetadatas: index.node.indexFieldMetadatas?.edges.map( - (indexField) => indexField.node, - ), - })), - })); + mockedStandardObjectMetadataQueryResult.objects.edges.map((edge) => { + const labelIdentifierFieldMetadataId = + objectMetadataItemSchema.shape.labelIdentifierFieldMetadataId.parse( + edge.node.labelIdentifierFieldMetadataId, + ); + + return { + ...edge.node, + fields: edge.node.fields.edges.map((edge) => edge.node), + labelIdentifierFieldMetadataId, + indexMetadatas: edge.node.indexMetadatas.edges.map((index) => ({ + ...index.node, + indexFieldMetadatas: index.node.indexFieldMetadatas?.edges.map( + (indexField) => indexField.node, + ), + })), + }; + });