Prevent relation update from settings (#13099)

## Expected behavior

Described behavior regarding: (update | create) x (custom | standard) x
(icon, label, name, isSynced)

**Custom:**
- Field RELATION create: name, label, isSynced, icon should be editable
- Field RELATION update: name should not, icon label, isSynced should
- For other fields, icon, label, name, isSynced should be editable at
field creation | update

To simplify: Field RELATION name should not be editable at update

**Standards**
- Field: create does not makes sense
- Field: name should not, icon label, isSynced should (this will end up
in overrides)

To simplify, no Field RELATION edge case, name should not be editable at
update

**Note:** the FE logic is quite different as the UI is hiding some
details behind the syncWithLabel. See my comments and TODO there


## What I've tested:
(update | create) x (custom | standard) x (icon, label, name, isSynced,
description)
This commit is contained in:
Charles Bochet
2025-07-08 21:03:38 +02:00
committed by GitHub
parent c8ec44eeaf
commit 39f6f3c4bb
13 changed files with 615 additions and 150 deletions

View File

@ -13,6 +13,7 @@ import { IconPicker } from '@/ui/input/components/IconPicker';
import { TextInput } from '@/ui/input/components/TextInput';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
AppTooltip,
@ -74,11 +75,11 @@ const StyledAdvancedSettingsContainer = styled.div`
type SettingsDataModelFieldIconLabelFormProps = {
fieldMetadataItem?: FieldMetadataItem;
maxLength?: number;
canToggleSyncLabelWithName?: boolean;
isCreationMode?: boolean;
};
export const SettingsDataModelFieldIconLabelForm = ({
canToggleSyncLabelWithName = true,
isCreationMode = false,
fieldMetadataItem,
maxLength,
}: SettingsDataModelFieldIconLabelFormProps) => {
@ -92,6 +93,8 @@ export const SettingsDataModelFieldIconLabelForm = ({
const theme = useTheme();
const label = watch('label');
const { t } = useLingui();
const labelTextInputId = `${fieldMetadataItem?.id}-label`;
@ -102,7 +105,6 @@ export const SettingsDataModelFieldIconLabelForm = ({
(isDefined(fieldMetadataItem)
? fieldMetadataItem.isLabelSyncedWithName
: true);
const label = watch('label');
const apiNameTooltipText = isLabelSyncedWithName
? t`Deactivate "Synchronize Objects Labels and API Names" to set a custom API name`
@ -115,6 +117,27 @@ export const SettingsDataModelFieldIconLabelForm = ({
});
};
const isRelation =
fieldMetadataItem?.type === FieldMetadataType.RELATION ||
fieldMetadataItem?.type === FieldMetadataType.MORPH_RELATION;
const isCustomButNotRelationField =
fieldMetadataItem?.isCustom === true && !isRelation;
// TODO: remove the custom RELATION edge case, this will result in canToggleSyncLabelWithName = isCustom
const canToggleSyncLabelWithName =
!isCreationMode && isCustomButNotRelationField;
// TODO: remove custom RELATION edge case, this will result in isNameEditEnabled = isCustom
const isNameEditEnabled =
isLabelSyncedWithName === false && isCustomButNotRelationField;
// TODO: remove custom RELATION edge case, this will result in isLabelEditEnabled = true
const isLabelEditEnabled =
isCreationMode ||
(!isCreationMode &&
(fieldMetadataItem?.isCustom === false || isCustomButNotRelationField));
return (
<>
<StyledInputsContainer>
@ -124,7 +147,7 @@ export const SettingsDataModelFieldIconLabelForm = ({
defaultValue={fieldMetadataItem?.icon ?? 'IconUsers'}
render={({ field: { onChange, value } }) => (
<IconPicker
selectedIconKey={value ?? ''}
selectedIconKey={value ?? 'IconUsers'}
onChange={({ iconKey }) => onChange(iconKey)}
variant="primary"
/>
@ -139,19 +162,19 @@ export const SettingsDataModelFieldIconLabelForm = ({
instanceId={labelTextInputId}
placeholder={t`Employees`}
value={value}
disabled={!isLabelEditEnabled}
onChange={(value) => {
onChange(value);
trigger('label');
if (isLabelSyncedWithName === true) {
if (
isCreationMode ||
(isLabelSyncedWithName === true &&
fieldMetadataItem?.isCustom === true)
) {
fillNameFromLabel(value);
}
}}
error={getErrorMessageFromError(errors.label?.message)}
disabled={
isLabelSyncedWithName === true &&
fieldMetadataItem &&
!fieldMetadataItem?.isCustom
}
maxLength={maxLength}
fullWidth
/>
@ -176,10 +199,7 @@ export const SettingsDataModelFieldIconLabelForm = ({
placeholder={t`employees`}
value={value}
onChange={onChange}
disabled={
(isLabelSyncedWithName ?? false) ||
!fieldMetadataItem?.isCustom
}
disabled={!isNameEditEnabled}
fullWidth
maxLength={DATABASE_IDENTIFIER_MAXIMUM_LENGTH}
RightIcon={() =>
@ -228,8 +248,20 @@ export const SettingsDataModelFieldIconLabelForm = ({
advancedMode
onChange={(value) => {
onChange(value);
if (value === true) {
if (!isDefined(fieldMetadataItem)) {
return;
}
if (value === false) {
return;
}
if (
fieldMetadataItem.isCustom === true &&
!isRelation
) {
fillNameFromLabel(label);
return;
}
}}
/>

View File

@ -215,10 +215,7 @@ export const SettingsObjectFieldEdit = () => {
<SettingsDataModelFieldIconLabelForm
fieldMetadataItem={fieldMetadataItem}
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
canToggleSyncLabelWithName={
fieldMetadataItem.type !== FieldMetadataType.RELATION &&
fieldMetadataItem.isCustom === true
}
isCreationMode={false}
/>
</Section>
{

View File

@ -221,9 +221,7 @@ export const SettingsObjectNewFieldConfigure = () => {
/>
<SettingsDataModelFieldIconLabelForm
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
canToggleSyncLabelWithName={
fieldType !== FieldMetadataType.RELATION
}
isCreationMode={true}
/>
</Section>
<Section>

View File

@ -141,33 +141,6 @@ describe('BeforeUpdateOneField', () => {
).rejects.toThrow(ValidationError);
});
it('should throw ValidationError when trying to update label when it is synced with name', async () => {
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
id: mockFieldId,
update: {
label: 'New Label',
},
};
const mockField: Partial<FieldMetadataEntity> = {
id: mockFieldId,
isCustom: false,
isLabelSyncedWithName: true,
label: 'Old Label',
};
jest
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
.mockResolvedValue(mockField as FieldMetadataEntity);
await expect(
hook.run(instance as UpdateOneInputType<UpdateFieldInput>, {
workspaceId: mockWorkspaceId,
locale: undefined,
}),
).rejects.toThrow(ValidationError);
});
it('should handle isActive updates for standard fields', async () => {
const instance: UpdateOneInputType<UpdateFieldInputForTest> = {
id: mockFieldId,

View File

@ -89,18 +89,6 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
!updatableFields.includes(key) && !overridableFields.includes(key),
);
const isUpdatingLabelWhenSynced =
instance.update.label &&
fieldMetadata.isLabelSyncedWithName &&
instance.update.isLabelSyncedWithName !== false &&
instance.update.label !== fieldMetadata.label;
if (isUpdatingLabelWhenSynced) {
throw new ValidationError(
'Cannot update label when it is synced with name',
);
}
if (nonUpdatableFields.length > 0) {
throw new ValidationError(
`Only isActive, isLabelSyncedWithName, label, icon, description and defaultValue fields can be updated for standard fields. Invalid fields: ${nonUpdatableFields.join(', ')}`,
@ -390,13 +378,6 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
update: StandardFieldUpdate,
locale?: keyof typeof APP_LOCALES,
): void {
if (
fieldMetadata.isLabelSyncedWithName ||
update.isLabelSyncedWithName === true
) {
return;
}
if (!isDefined(instance.update.label)) {
return;
}

View File

@ -9,10 +9,10 @@ import {
Max,
Min,
ValidationError,
isDefined,
validateOrReject,
} from 'class-validator';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
@ -187,5 +187,20 @@ export class FieldMetadataValidationService {
settings: fieldMetadataInput.settings,
});
}
const isRelationField =
fieldMetadataType === FieldMetadataType.RELATION ||
fieldMetadataType === FieldMetadataType.MORPH_RELATION;
if (
isRelationField &&
isDefined(existingFieldMetadata) &&
fieldMetadataInput.name !== existingFieldMetadata.name
) {
throw new FieldMetadataException(
'Name cannot be changed for relation fields',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
}
}
}

View File

@ -0,0 +1,389 @@
import { i18n } from '@lingui/core';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { resolveOverridableString } from 'src/engine/metadata-modules/field-metadata/utils/resolve-overridable-string.util';
jest.mock('@lingui/core');
jest.mock('src/engine/core-modules/i18n/utils/generateMessageId');
const mockI18n = i18n as jest.Mocked<typeof i18n>;
const mockGenerateMessageId = generateMessageId as jest.MockedFunction<
typeof generateMessageId
>;
describe('resolveOverridableString', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Custom fields', () => {
it('should return the field value for custom label field', () => {
const fieldMetadata = {
label: 'Custom Label',
description: 'Custom Description',
icon: 'custom-icon',
isCustom: true,
standardOverrides: undefined,
};
const result = resolveOverridableString(fieldMetadata, 'label', 'fr-FR');
expect(result).toBe('Custom Label');
});
it('should return the field value for custom description field', () => {
const fieldMetadata = {
label: 'Custom Label',
description: 'Custom Description',
icon: 'custom-icon',
isCustom: true,
standardOverrides: undefined,
};
const result = resolveOverridableString(
fieldMetadata,
'description',
undefined,
);
expect(result).toBe('Custom Description');
});
it('should return the field value for custom icon field', () => {
const fieldMetadata = {
label: 'Custom Label',
description: 'Custom Description',
icon: 'custom-icon',
isCustom: true,
standardOverrides: undefined,
};
const result = resolveOverridableString(
fieldMetadata,
'icon',
SOURCE_LOCALE,
);
expect(result).toBe('custom-icon');
});
});
describe('Standard fields - Icon overrides', () => {
it('should return override icon when available for standard field', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {
icon: 'override-icon',
},
};
const result = resolveOverridableString(fieldMetadata, 'icon', 'fr-FR');
expect(result).toBe('override-icon');
});
});
describe('Standard fields - Translation overrides', () => {
it('should return translation override when available for non-icon fields', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {
translations: {
'fr-FR': {
label: 'Libellé traduit',
description: 'Description traduite',
},
},
},
};
expect(resolveOverridableString(fieldMetadata, 'label', 'fr-FR')).toBe(
'Libellé traduit',
);
expect(
resolveOverridableString(fieldMetadata, 'description', 'fr-FR'),
).toBe('Description traduite');
});
it('should fallback when translation override is not available for the locale', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {
translations: {
'es-ES': {
label: 'Etiqueta en español',
},
},
},
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveOverridableString(fieldMetadata, 'label', 'fr-FR');
expect(result).toBe('Standard Label');
});
it('should fallback when translation override is not available for the labelKey', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {
translations: {
'fr-FR': {
label: 'Libellé traduit',
},
},
},
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveOverridableString(
fieldMetadata,
'description',
'fr-FR',
);
expect(result).toBe('Standard Description');
});
it('should not use translation overrides when locale is undefined', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {
translations: {
'fr-FR': {
label: 'Libellé traduit',
},
},
},
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveOverridableString(
fieldMetadata,
'label',
undefined,
);
expect(result).toBe('Standard Label');
});
});
describe('Standard fields - SOURCE_LOCALE overrides', () => {
it('should return direct override for SOURCE_LOCALE when available', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {
label: 'Overridden Label',
description: 'Overridden Description',
icon: 'overridden-icon',
},
};
expect(
resolveOverridableString(fieldMetadata, 'label', SOURCE_LOCALE),
).toBe('Overridden Label');
expect(
resolveOverridableString(fieldMetadata, 'description', SOURCE_LOCALE),
).toBe('Overridden Description');
expect(
resolveOverridableString(fieldMetadata, 'icon', SOURCE_LOCALE),
).toBe('overridden-icon');
});
it('should not use direct override for non-SOURCE_LOCALE', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {
label: 'Overridden Label',
},
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveOverridableString(fieldMetadata, 'label', 'fr-FR');
expect(result).toBe('Standard Label');
});
it('should not use empty string override for SOURCE_LOCALE', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {
label: '',
},
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveOverridableString(
fieldMetadata,
'label',
SOURCE_LOCALE,
);
expect(result).toBe('Standard Label');
});
it('should not use undefined override for SOURCE_LOCALE', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {
label: undefined,
},
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveOverridableString(
fieldMetadata,
'label',
SOURCE_LOCALE,
);
expect(result).toBe('Standard Label');
});
});
describe('Standard fields - Auto translation fallback', () => {
it('should return translated message when translation is available', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: undefined,
};
mockGenerateMessageId.mockReturnValue('standard.label.message.id');
mockI18n._.mockReturnValue('Libellé traduit automatiquement');
const result = resolveOverridableString(fieldMetadata, 'label', 'fr-FR');
expect(mockGenerateMessageId).toHaveBeenCalledWith('Standard Label');
expect(mockI18n._).toHaveBeenCalledWith('standard.label.message.id');
expect(result).toBe('Libellé traduit automatiquement');
});
it('should return original field value when no translation is found', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: undefined,
};
const messageId = 'standard.label.message.id';
mockGenerateMessageId.mockReturnValue(messageId);
mockI18n._.mockReturnValue(messageId);
const result = resolveOverridableString(fieldMetadata, 'label', 'fr-FR');
expect(result).toBe('Standard Label');
});
});
describe('Priority order - Standard fields', () => {
it('should prioritize translation override over SOURCE_LOCALE override for non-SOURCE_LOCALE', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {
label: 'Source Override',
translations: {
'fr-FR': {
label: 'Translation Override',
},
},
},
};
const result = resolveOverridableString(fieldMetadata, 'label', 'fr-FR');
expect(result).toBe('Translation Override');
expect(mockGenerateMessageId).not.toHaveBeenCalled();
expect(mockI18n._).not.toHaveBeenCalled();
});
it('should prioritize SOURCE_LOCALE override over auto translation for SOURCE_LOCALE', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {
label: 'Source Override',
},
};
const result = resolveOverridableString(
fieldMetadata,
'label',
SOURCE_LOCALE,
);
expect(result).toBe('Source Override');
expect(mockGenerateMessageId).not.toHaveBeenCalled();
expect(mockI18n._).not.toHaveBeenCalled();
});
it('should use auto translation when no overrides are available', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {},
};
mockGenerateMessageId.mockReturnValue('auto.translation.id');
mockI18n._.mockReturnValue('Auto Translated Label');
const result = resolveOverridableString(fieldMetadata, 'label', 'de-DE');
expect(result).toBe('Auto Translated Label');
expect(mockGenerateMessageId).toHaveBeenCalledWith('Standard Label');
expect(mockI18n._).toHaveBeenCalledWith('auto.translation.id');
});
});
});

View File

@ -1,6 +1,7 @@
import { i18n } from '@lingui/core';
import { isDefined } from 'class-validator';
import { APP_LOCALES } from 'twenty-shared/translations';
import { isNonEmptyString } from '@sniptt/guards';
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import { isDefined } from 'twenty-shared/utils';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
@ -17,13 +18,29 @@ export const resolveOverridableString = (
return fieldMetadata[labelKey] ?? '';
}
if (labelKey === 'icon' && isDefined(fieldMetadata.standardOverrides?.icon)) {
return fieldMetadata.standardOverrides.icon;
}
if (
isDefined(fieldMetadata.standardOverrides?.translations) &&
isDefined(locale) &&
labelKey !== 'icon'
) {
const translationValue =
// @ts-expect-error legacy noImplicitAny
fieldMetadata.standardOverrides?.translations?.[locale]?.[labelKey];
fieldMetadata.standardOverrides.translations[locale]?.[labelKey];
if (isDefined(translationValue)) {
return translationValue;
}
}
if (
locale === SOURCE_LOCALE &&
isNonEmptyString(fieldMetadata.standardOverrides?.[labelKey])
) {
return fieldMetadata.standardOverrides[labelKey] ?? '';
}
const messageId = generateMessageId(fieldMetadata[labelKey] ?? '');
const translatedMessage = i18n._(messageId);

View File

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Field metadata relation update should fail relation when name is changed 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "Name cannot be changed for relation fields",
"name": "UserInputError",
},
]
`;
exports[`Field metadata relation update should fail relation when name is not in camel case 1`] = `
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "An error occurred.",
},
"message": "New Name should be in camelCase",
"name": "UserInputError",
},
]
`;

View File

@ -0,0 +1,110 @@
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
import { EachTestingContext } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
type UpdateOneFieldMetadataTestingContext = EachTestingContext<{
name: string;
}>;
const globalTestContext = {
employeeObjectId: '',
enterpriseObjectId: '',
employerFieldMetadataId: '',
};
describe('Field metadata relation update should fail', () => {
const failingRelationUpdateTestsUseCase: UpdateOneFieldMetadataTestingContext[] =
[
{
title: 'when name is not in camel case',
context: { name: 'New Name' },
},
{
title: 'when name is changed',
context: { name: 'newName' },
},
];
beforeAll(async () => {
const {
data: {
createOneObject: { id: employeeObjectId },
},
} = await createOneObjectMetadata({
input: getMockCreateObjectInput({
namePlural: 'employees',
nameSingular: 'employee',
}),
});
const {
data: {
createOneObject: { id: enterpriseObjectId },
},
} = await createOneObjectMetadata({
input: getMockCreateObjectInput({
namePlural: 'enterprises',
nameSingular: 'enterprise',
}),
});
const { data } = await createOneFieldMetadata({
input: {
objectMetadataId: employeeObjectId,
name: 'employer',
label: 'Employer',
isLabelSyncedWithName: false,
type: FieldMetadataType.RELATION,
relationCreationPayload: {
targetFieldLabel: 'employees',
type: RelationType.MANY_TO_ONE,
targetObjectMetadataId: enterpriseObjectId,
targetFieldIcon: 'IconBuildingSkyscraper',
},
},
});
globalTestContext.employeeObjectId = employeeObjectId;
globalTestContext.enterpriseObjectId = enterpriseObjectId;
globalTestContext.employerFieldMetadataId = data.createOneField.id;
expect(data).toBeDefined();
});
afterAll(async () => {
for (const objectMetadataId of [
globalTestContext.employeeObjectId,
globalTestContext.enterpriseObjectId,
]) {
await deleteOneObjectMetadata({
input: {
idToDelete: objectMetadataId,
},
});
}
});
it.each(failingRelationUpdateTestsUseCase)(
'relation $title',
async ({ context }) => {
const { errors } = await updateOneFieldMetadata({
expectToFail: true,
input: {
idToUpdate: globalTestContext.employerFieldMetadataId,
updatePayload: {
name: context.name,
},
},
});
expect(errors).toBeDefined();
expect(errors).toMatchSnapshot();
},
);
});

View File

@ -116,78 +116,4 @@ describe('updateOne', () => {
);
});
});
describe('FieldMetadataService Enum Default Value Validation', () => {
let createdObjectMetadataId: string;
beforeEach(async () => {
const { data: listingObjectMetadata } = await createOneObjectMetadata({
input: {
labelSingular: LISTING_NAME_SINGULAR,
labelPlural: LISTING_NAME_PLURAL,
nameSingular: LISTING_NAME_SINGULAR,
namePlural: LISTING_NAME_PLURAL,
icon: 'IconBuildingSkyscraper',
isLabelSyncedWithName: true,
},
});
createdObjectMetadataId = listingObjectMetadata.createOneObject.id;
});
afterEach(async () => {
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataId },
});
});
it('should throw an error if the default value is not in the options', async () => {
const { data: createdFieldMetadata } = await createOneFieldMetadata({
input: {
objectMetadataId: createdObjectMetadataId,
type: FieldMetadataType.SELECT,
name: 'testName',
label: 'Test name',
isLabelSyncedWithName: true,
options: [
{
label: 'Option 1',
value: 'OPTION_1',
color: 'green',
position: 1,
},
],
},
});
const { errors } = await updateOneFieldMetadata({
input: {
idToUpdate: createdFieldMetadata.createOneField.id,
updatePayload: {
defaultValue: "'OPTION_2'",
},
},
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
expectToFail: true,
});
expect(errors).toMatchInlineSnapshot(`
[
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Default value "'OPTION_2'" must be one of the option values",
},
"message": "Default value "'OPTION_2'" must be one of the option values",
"name": "UserInputError",
},
]
`);
});
});
});