From a59ed804226e50adea45e218d91850f0d4cd897f Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 19 Jun 2026 15:07:37 +0200 Subject: [PATCH] fix(front): allow null subfields in Phones default value so Save enables (#21847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Closes #21780. When editing a **Phones** field in Settings → Data Model, changing the **Default Country Code** does not enable the Save button — the form becomes dirty but never valid, so the change can't be saved. ## Root cause The settings form validates `defaultValue` with the record-value `phonesFieldValueSchema`, which requires non-null strings: ```ts primaryPhoneNumber: z.string(), primaryPhoneCountryCode: z.string(), ``` But a Phones default value can legitimately have **null** subfields — a default country code with no default number. The backend normalizes empty subfields to `null` (`nullify-empty-phones-default-value.util.ts`), and the shared contract `FieldMetadataDefaultValuePhones` is `string | null`. So an existing field whose stored default has `primaryPhoneNumber: null` makes the form **permanently invalid**: changing the country code preserves the null number → `isValid` stays `false` → `canSave = isDirty && isValid` keeps Save disabled. The sibling **address** field doesn't have this bug because `addressFieldValueSchema` already makes every subfield `.nullable()`. Phones was simply inconsistent. ## Fix Add a dedicated `phonesFieldDefaultValueSchema` with nullable subfields (mirroring the address pattern and matching `FieldMetadataDefaultValuePhones`) and use it in the Phones settings form. The stricter record-value `phonesFieldValueSchema` is left untouched, so record input/persistence/empty-checks are unaffected. ## Test plan - [x] Unit test covering the partial-null default value (and asserting the record-value schema still rejects it) - [x] `nx typecheck twenty-front` clean - [x] `nx lint:diff-with-main twenty-front` clean - Manual: open a Phones field, set a Default Country Code and save, re-open, change the country code → Save now enables. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Review in cubic Co-authored-by: Claude Opus 4.8 --- .../phonesFieldDefaultValueSchema.test.ts | 27 +++++++++++++++++++ .../phonesFieldDefaultValueSchema.ts | 16 +++++++++++ .../SettingsDataModelFieldPhonesForm.tsx | 6 ++--- 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/__tests__/phonesFieldDefaultValueSchema.test.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/phonesFieldDefaultValueSchema.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/__tests__/phonesFieldDefaultValueSchema.test.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/__tests__/phonesFieldDefaultValueSchema.test.ts new file mode 100644 index 0000000000..ff311c3f2b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/__tests__/phonesFieldDefaultValueSchema.test.ts @@ -0,0 +1,27 @@ +import { phonesFieldDefaultValueSchema } from '@/object-record/record-field/ui/validation-schemas/phonesFieldDefaultValueSchema'; +import { phonesFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/phonesFieldValueSchema'; + +describe('phonesFieldDefaultValueSchema', () => { + it('should accept a fully populated default value', () => { + const value = { + primaryPhoneNumber: "'123456'", + primaryPhoneCountryCode: "'FR'", + primaryPhoneCallingCode: "'33'", + additionalPhones: null, + }; + + expect(phonesFieldDefaultValueSchema.safeParse(value).success).toBe(true); + }); + + it('should accept a default value with null primary phone subfields', () => { + const value = { + primaryPhoneNumber: null, + primaryPhoneCountryCode: "'FR'", + primaryPhoneCallingCode: "'33'", + additionalPhones: null, + }; + + expect(phonesFieldDefaultValueSchema.safeParse(value).success).toBe(true); + expect(phonesFieldValueSchema.safeParse(value).success).toBe(false); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/phonesFieldDefaultValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/phonesFieldDefaultValueSchema.ts new file mode 100644 index 0000000000..a444d0dfdd --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/phonesFieldDefaultValueSchema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +export const phonesFieldDefaultValueSchema = z.object({ + primaryPhoneNumber: z.string().nullable(), + primaryPhoneCountryCode: z.string().nullable(), + primaryPhoneCallingCode: z.string().nullable().optional(), + additionalPhones: z + .array( + z.object({ + number: z.string(), + callingCode: z.string(), + countryCode: z.string(), + }), + ) + .nullable(), +}); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx index 365215a080..2a088613b8 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx @@ -1,7 +1,7 @@ import { Controller, useFormContext } from 'react-hook-form'; import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; -import { phonesFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/phonesFieldValueSchema'; +import { phonesFieldDefaultValueSchema } from '@/object-record/record-field/ui/validation-schemas/phonesFieldDefaultValueSchema'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; import { mergeSettingsSchemas } from '@/settings/data-model/fields/forms/utils/mergeSettingsSchema'; import { settingsDataModelFieldMaxValuesSchema } from '@/settings/data-model/fields/forms/utils/settingsDataModelFieldMaxValuesSchema'; @@ -28,7 +28,7 @@ type SettingsDataModelFieldPhonesFormProps = { export const settingsDataModelFieldPhonesFormSchema = z .object({ - defaultValue: phonesFieldValueSchema, + defaultValue: phonesFieldDefaultValueSchema, }) .merge( mergeSettingsSchemas( @@ -91,7 +91,7 @@ export const SettingsDataModelFieldPhonesForm = ({ dropdownId="selectDefaultCountryCode" value={stripSimpleQuotesFromString( - value?.primaryPhoneCountryCode, + value?.primaryPhoneCountryCode ?? '', )} onChange={(newPhoneCountryCode) => onChange({