[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 <charles@twenty.com>
This commit is contained in:
Paul Rastoin
2025-01-22 16:32:57 +01:00
committed by GitHub
parent 80c9ebfd4e
commit 8ab01ebef4
22 changed files with 161 additions and 89 deletions

View File

@ -81,6 +81,6 @@ export const responseData = {
isActive: true,
createdAt: '',
updatedAt: '',
labelIdentifierFieldMetadataId: '',
labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1',
imageIdentifierFieldMetadataId: '',
};

View File

@ -36,6 +36,6 @@ export const responseData = {
isActive: true,
createdAt: '',
updatedAt: '',
labelIdentifierFieldMetadataId: '',
labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1',
imageIdentifierFieldMetadataId: '',
};

View File

@ -50,6 +50,6 @@ export const responseData = {
isActive: true,
createdAt: '',
updatedAt: '',
labelIdentifierFieldMetadataId: '',
labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1',
imageIdentifierFieldMetadataId: '',
};

View File

@ -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[];
};

View File

@ -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);
});
});

View File

@ -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;
};

View File

@ -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<keyof ObjectMetadataItem, unknown>
> = {
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);
});
});

View File

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