fix(front): allow null subfields in Phones default value so Save enables (#21847)
## 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) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21847?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+27
@@ -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);
|
||||
});
|
||||
});
|
||||
+16
@@ -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(),
|
||||
});
|
||||
+3
-3
@@ -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 = ({
|
||||
<Select<string>
|
||||
dropdownId="selectDefaultCountryCode"
|
||||
value={stripSimpleQuotesFromString(
|
||||
value?.primaryPhoneCountryCode,
|
||||
value?.primaryPhoneCountryCode ?? '',
|
||||
)}
|
||||
onChange={(newPhoneCountryCode) =>
|
||||
onChange({
|
||||
|
||||
Reference in New Issue
Block a user