[REFACTOR] Split in two distinct forms Settings Object Model page (#10653)
# Introduction This PR contains around ~+300 tests + snapshot additions Please check both object model creation and edition Closes https://github.com/twentyhq/core-team-issues/issues/355 Refactored into two agnostic forms the Object Model settings page for instance `/settings/objects/notes#settings`. ## `SettingsDataModelObjectAboutForm` Added a new abstraction `SettingsUpdateDataModelObjectAboutForm` to wrap `SettingsDataModelObjectAboutForm` in an `update` context  Schema: ```ts const requiredFormFields = objectMetadataItemSchema.pick({ description: true, icon: true, labelPlural: true, labelSingular: true, }); const optionalFormFields = objectMetadataItemSchema .pick({ nameSingular: true, namePlural: true, isLabelSyncedWithName: true, }) .partial(); export const settingsDataModelObjectAboutFormSchema = requiredFormFields.merge(optionalFormFields); ``` ## `SettingsDataModelObjectSettingsFormCard` Update on change  Schema: ```ts export const settingsDataModelObjectIdentifiersFormSchema = objectMetadataItemSchema.pick({ labelIdentifierFieldMetadataId: true, imageIdentifierFieldMetadataId: true, }); ``` ## Error management and validation schema Improved the frontend validation form in order to attest that: - Names are in camelCase - Names are differents - Names are not empty string ***SHOULD BE DONE SERVER SIDE TOO*** ( will in a next PR, atm it literally breaks any workspace ) - Labels are differents - Labels aren't empty strings Hide the error messages as we need to decide what kind of styling we want for our errors with forms ( Example with error labels ) 
This commit is contained in:
@ -0,0 +1,159 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`settingsDataModelObjectAboutFormSchema fails when isLabelSyncedWithName is not a boolean 1`] = `
|
||||
[ZodError: [
|
||||
{
|
||||
"code": "invalid_type",
|
||||
"expected": "boolean",
|
||||
"received": "string",
|
||||
"path": [
|
||||
"isLabelSyncedWithName"
|
||||
],
|
||||
"message": "Expected boolean, received string"
|
||||
}
|
||||
]]
|
||||
`;
|
||||
|
||||
exports[`settingsDataModelObjectAboutFormSchema fails when labels are empty strings 1`] = `
|
||||
[ZodError: [
|
||||
{
|
||||
"code": "too_small",
|
||||
"minimum": 1,
|
||||
"type": "string",
|
||||
"inclusive": true,
|
||||
"exact": false,
|
||||
"message": "String must contain at least 1 character(s)",
|
||||
"path": [
|
||||
"labelSingular"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "too_small",
|
||||
"minimum": 1,
|
||||
"type": "string",
|
||||
"inclusive": true,
|
||||
"exact": false,
|
||||
"message": "String must contain at least 1 character(s)",
|
||||
"path": [
|
||||
"labelPlural"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "Singular and plural labels must be different",
|
||||
"path": [
|
||||
"labelPlural"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "Singular and plural labels must be different",
|
||||
"path": [
|
||||
"labelSingular"
|
||||
]
|
||||
}
|
||||
]]
|
||||
`;
|
||||
|
||||
exports[`settingsDataModelObjectAboutFormSchema fails when names are not in camelCase 1`] = `
|
||||
[ZodError: [
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "String should be camel case",
|
||||
"path": [
|
||||
"namePlural"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "String should be camel case",
|
||||
"path": [
|
||||
"nameSingular"
|
||||
]
|
||||
}
|
||||
]]
|
||||
`;
|
||||
|
||||
exports[`settingsDataModelObjectAboutFormSchema fails when required fields are missing 1`] = `
|
||||
[ZodError: [
|
||||
{
|
||||
"code": "invalid_type",
|
||||
"expected": "string",
|
||||
"received": "undefined",
|
||||
"path": [
|
||||
"labelSingular"
|
||||
],
|
||||
"message": "Required"
|
||||
},
|
||||
{
|
||||
"code": "invalid_type",
|
||||
"expected": "string",
|
||||
"received": "undefined",
|
||||
"path": [
|
||||
"labelPlural"
|
||||
],
|
||||
"message": "Required"
|
||||
}
|
||||
]]
|
||||
`;
|
||||
|
||||
exports[`settingsDataModelObjectAboutFormSchema fails when singular and plural labels are the same 1`] = `
|
||||
[ZodError: [
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "Singular and plural labels must be different",
|
||||
"path": [
|
||||
"labelPlural"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "Singular and plural labels must be different",
|
||||
"path": [
|
||||
"labelSingular"
|
||||
]
|
||||
}
|
||||
]]
|
||||
`;
|
||||
|
||||
exports[`settingsDataModelObjectAboutFormSchema fails when singular and plural names are the same 1`] = `
|
||||
[ZodError: [
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "Singular and plural names must be different",
|
||||
"path": [
|
||||
"nameSingular"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "Singular and plural names must be different",
|
||||
"path": [
|
||||
"namePlural"
|
||||
]
|
||||
}
|
||||
]]
|
||||
`;
|
||||
|
||||
exports[`settingsDataModelObjectAboutFormSchema fails with invalid types for optional fields 1`] = `
|
||||
[ZodError: [
|
||||
{
|
||||
"code": "invalid_type",
|
||||
"expected": "string",
|
||||
"received": "number",
|
||||
"path": [
|
||||
"description"
|
||||
],
|
||||
"message": "Expected string, received number"
|
||||
},
|
||||
{
|
||||
"code": "invalid_type",
|
||||
"expected": "string",
|
||||
"received": "boolean",
|
||||
"path": [
|
||||
"icon"
|
||||
],
|
||||
"message": "Expected string, received boolean"
|
||||
}
|
||||
]]
|
||||
`;
|
||||
@ -1,51 +0,0 @@
|
||||
import { SafeParseSuccess } from 'zod';
|
||||
|
||||
import { CreateObjectInput } from '~/generated-metadata/graphql';
|
||||
|
||||
import { settingsCreateObjectInputSchema } from '../settingsCreateObjectInputSchema';
|
||||
|
||||
describe('settingsCreateObjectInputSchema', () => {
|
||||
it('validates a valid input and adds name properties', () => {
|
||||
// Given
|
||||
const validInput = {
|
||||
description: 'A valid description',
|
||||
icon: 'IconPlus',
|
||||
labelPlural: ' Labels ',
|
||||
labelSingular: 'Label ',
|
||||
namePlural: 'namePlural',
|
||||
nameSingular: 'nameSingular',
|
||||
isLabelSyncedWithName: false,
|
||||
};
|
||||
|
||||
// When
|
||||
const result = settingsCreateObjectInputSchema.safeParse(validInput);
|
||||
|
||||
// Then
|
||||
expect(result.success).toBe(true);
|
||||
expect((result as SafeParseSuccess<CreateObjectInput>).data).toEqual({
|
||||
description: validInput.description,
|
||||
icon: validInput.icon,
|
||||
labelPlural: 'Labels',
|
||||
labelSingular: 'Label',
|
||||
namePlural: 'namePlural',
|
||||
nameSingular: 'nameSingular',
|
||||
isLabelSyncedWithName: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('fails for an invalid input', () => {
|
||||
// Given
|
||||
const invalidInput = {
|
||||
description: 123,
|
||||
icon: true,
|
||||
labelPlural: [],
|
||||
labelSingular: {},
|
||||
};
|
||||
|
||||
// When
|
||||
const result = settingsCreateObjectInputSchema.safeParse(invalidInput);
|
||||
|
||||
// Then
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,150 @@
|
||||
import {
|
||||
SettingsDataModelObjectAboutFormValues,
|
||||
settingsDataModelObjectAboutFormSchema,
|
||||
} from '@/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema';
|
||||
import { EachTestingContext } from '~/types/EachTestingContext';
|
||||
|
||||
describe('settingsDataModelObjectAboutFormSchema', () => {
|
||||
const validInput: SettingsDataModelObjectAboutFormValues = {
|
||||
description: 'A valid description',
|
||||
icon: 'IconName',
|
||||
labelPlural: 'Labels Plural',
|
||||
labelSingular: 'Label Singular',
|
||||
namePlural: 'labelsPlural',
|
||||
nameSingular: 'labelSingular',
|
||||
isLabelSyncedWithName: false,
|
||||
};
|
||||
|
||||
const passingTestsUseCase: EachTestingContext<{
|
||||
input: SettingsDataModelObjectAboutFormValues;
|
||||
expectedSuccess: true;
|
||||
}>[] = [
|
||||
{
|
||||
title: 'validates a complete valid input',
|
||||
context: {
|
||||
input: validInput,
|
||||
expectedSuccess: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'validates input with optional fields omitted',
|
||||
context: {
|
||||
input: {
|
||||
labelPlural: 'Labels Plural',
|
||||
labelSingular: 'Label Singular',
|
||||
namePlural: 'labelsPlural',
|
||||
nameSingular: 'labelSingular',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
expectedSuccess: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'validates input with trimmed labels',
|
||||
context: {
|
||||
input: {
|
||||
...validInput,
|
||||
labelPlural: ' Labels Plural ',
|
||||
labelSingular: ' Label Singular ',
|
||||
},
|
||||
expectedSuccess: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const failsValidationTestsUseCase: EachTestingContext<{
|
||||
input: Partial<Record<keyof SettingsDataModelObjectAboutFormValues, any>>;
|
||||
expectedSuccess: false;
|
||||
}>[] = [
|
||||
{
|
||||
title: 'fails when required fields are missing',
|
||||
context: {
|
||||
input: {
|
||||
description: 'Only description',
|
||||
labelPlural: undefined,
|
||||
labelSingular: undefined,
|
||||
},
|
||||
expectedSuccess: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'fails when names are not in camelCase',
|
||||
context: {
|
||||
input: {
|
||||
...validInput,
|
||||
namePlural: 'Labels_Plural',
|
||||
nameSingular: 'Label-Singular',
|
||||
},
|
||||
expectedSuccess: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'fails when labels are empty strings',
|
||||
context: {
|
||||
input: {
|
||||
...validInput,
|
||||
labelPlural: '',
|
||||
labelSingular: '',
|
||||
},
|
||||
expectedSuccess: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'fails when singular and plural labels are the same',
|
||||
context: {
|
||||
input: {
|
||||
...validInput,
|
||||
labelPlural: 'Same Label',
|
||||
labelSingular: 'Same Label',
|
||||
},
|
||||
expectedSuccess: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'fails when singular and plural names are the same',
|
||||
context: {
|
||||
input: {
|
||||
...validInput,
|
||||
namePlural: 'sameName',
|
||||
nameSingular: 'sameName',
|
||||
},
|
||||
expectedSuccess: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'fails when isLabelSyncedWithName is not a boolean',
|
||||
context: {
|
||||
input: {
|
||||
...validInput,
|
||||
isLabelSyncedWithName: 'true',
|
||||
},
|
||||
expectedSuccess: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'fails with invalid types for optional fields',
|
||||
context: {
|
||||
input: {
|
||||
...validInput,
|
||||
description: 123,
|
||||
icon: true,
|
||||
},
|
||||
expectedSuccess: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each([...passingTestsUseCase, ...failsValidationTestsUseCase])(
|
||||
'$title',
|
||||
({ context: { expectedSuccess, input } }) => {
|
||||
const result = settingsDataModelObjectAboutFormSchema.safeParse({
|
||||
...validInput,
|
||||
...input,
|
||||
});
|
||||
expect(result.success).toBe(expectedSuccess);
|
||||
if (!expectedSuccess) {
|
||||
expect(result.error).toMatchSnapshot();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -1,52 +0,0 @@
|
||||
import { SafeParseSuccess } from 'zod';
|
||||
|
||||
import { UpdateObjectPayload } from '~/generated-metadata/graphql';
|
||||
|
||||
import { settingsUpdateObjectInputSchema } from '../settingsUpdateObjectInputSchema';
|
||||
|
||||
describe('settingsUpdateObjectInputSchema', () => {
|
||||
it('validates a valid input and adds name properties', () => {
|
||||
// Given
|
||||
const validInput = {
|
||||
description: 'A valid description',
|
||||
icon: 'IconName',
|
||||
labelPlural: 'Labels Plural ',
|
||||
labelSingular: ' Label Singular',
|
||||
namePlural: 'namePlural',
|
||||
nameSingular: 'nameSingular',
|
||||
labelIdentifierFieldMetadataId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
|
||||
};
|
||||
|
||||
// When
|
||||
const result = settingsUpdateObjectInputSchema.safeParse(validInput);
|
||||
|
||||
// Then
|
||||
expect(result.success).toBe(true);
|
||||
expect((result as SafeParseSuccess<UpdateObjectPayload>).data).toEqual({
|
||||
description: validInput.description,
|
||||
icon: validInput.icon,
|
||||
labelIdentifierFieldMetadataId: validInput.labelIdentifierFieldMetadataId,
|
||||
labelPlural: 'Labels Plural',
|
||||
labelSingular: 'Label Singular',
|
||||
namePlural: 'namePlural',
|
||||
nameSingular: 'nameSingular',
|
||||
});
|
||||
});
|
||||
|
||||
it('fails for an invalid input', () => {
|
||||
// Given
|
||||
const invalidInput = {
|
||||
description: 123,
|
||||
icon: true,
|
||||
labelPlural: [],
|
||||
labelSingular: {},
|
||||
labelIdentifierFieldMetadataId: 'invalid uuid',
|
||||
};
|
||||
|
||||
// When
|
||||
const result = settingsUpdateObjectInputSchema.safeParse(invalidInput);
|
||||
|
||||
// Then
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -1,16 +0,0 @@
|
||||
import { settingsDataModelObjectAboutFormSchema } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
||||
import { CreateObjectInput } from '~/generated-metadata/graphql';
|
||||
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||
|
||||
export const settingsCreateObjectInputSchema =
|
||||
settingsDataModelObjectAboutFormSchema.transform<CreateObjectInput>(
|
||||
(values) => ({
|
||||
...values,
|
||||
nameSingular:
|
||||
values.nameSingular ??
|
||||
computeMetadataNameFromLabel(values.labelSingular),
|
||||
namePlural:
|
||||
values.namePlural ?? computeMetadataNameFromLabel(values.labelPlural),
|
||||
isLabelSyncedWithName: values.isLabelSyncedWithName ?? true,
|
||||
}),
|
||||
);
|
||||
@ -0,0 +1,68 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { ZodType, z } from 'zod';
|
||||
import { ReadonlyKeysArray } from '~/types/ReadonlyKeysArray';
|
||||
import { zodNonEmptyString } from '~/types/ZodNonEmptyString';
|
||||
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';
|
||||
|
||||
type ZodTypeSettingsDataModelFormFields = ZodType<
|
||||
Pick<
|
||||
ObjectMetadataItem,
|
||||
| 'labelSingular'
|
||||
| 'labelPlural'
|
||||
| 'description'
|
||||
| 'icon'
|
||||
| 'namePlural'
|
||||
| 'nameSingular'
|
||||
| 'isLabelSyncedWithName'
|
||||
>
|
||||
>;
|
||||
const settingsDataModelFormFieldsSchema = z.object({
|
||||
description: z.string().nullish(),
|
||||
icon: z.string().optional(),
|
||||
labelSingular: zodNonEmptyString,
|
||||
labelPlural: zodNonEmptyString,
|
||||
namePlural: zodNonEmptyString.and(camelCaseStringSchema),
|
||||
nameSingular: zodNonEmptyString.and(camelCaseStringSchema),
|
||||
isLabelSyncedWithName: z.boolean(),
|
||||
}) satisfies ZodTypeSettingsDataModelFormFields;
|
||||
|
||||
export const settingsDataModelObjectAboutFormSchema =
|
||||
settingsDataModelFormFieldsSchema.superRefine(
|
||||
({ labelPlural, labelSingular, namePlural, nameSingular }, ctx) => {
|
||||
const labelsAreDifferent =
|
||||
labelPlural.trim().toLowerCase() !== labelSingular.trim().toLowerCase();
|
||||
if (!labelsAreDifferent) {
|
||||
const labelFields: ReadonlyKeysArray<ObjectMetadataItem> = [
|
||||
'labelPlural',
|
||||
'labelSingular',
|
||||
];
|
||||
labelFields.forEach((field) =>
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t`Singular and plural labels must be different`,
|
||||
path: [field],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const nameAreDifferent =
|
||||
nameSingular.toLowerCase() !== namePlural.toLowerCase();
|
||||
if (!nameAreDifferent) {
|
||||
const nameFields: ReadonlyKeysArray<ObjectMetadataItem> = [
|
||||
'nameSingular',
|
||||
'namePlural',
|
||||
];
|
||||
nameFields.forEach((field) =>
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t`Singular and plural names must be different`,
|
||||
path: [field],
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
export type SettingsDataModelObjectAboutFormValues = z.infer<
|
||||
typeof settingsDataModelObjectAboutFormSchema
|
||||
>;
|
||||
@ -1,13 +0,0 @@
|
||||
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
||||
import { settingsDataModelObjectAboutFormSchema } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
||||
|
||||
export const settingsUpdateObjectInputSchema =
|
||||
settingsDataModelObjectAboutFormSchema
|
||||
.merge(
|
||||
objectMetadataItemSchema.pick({
|
||||
imageIdentifierFieldMetadataId: true,
|
||||
isActive: true,
|
||||
labelIdentifierFieldMetadataId: true,
|
||||
}),
|
||||
)
|
||||
.partial();
|
||||
Reference in New Issue
Block a user