fix(address): show saved address in record detail when street1 is null (#21033)
## Fixes #20084 ### Problem A saved address is visible in the **table view** but shows **"Empty"** in the **record detail page** when `addressStreet1` is `null`. This reproduces with the default seed data out of the box — e.g. **Google** (city "Mountain View", no street), **Microsoft** (Redmond), **Meta** (Menlo Park) — which is why several users reported hitting it immediately. ### Root cause The frontend zod schema required `addressStreet1` to be a **non-null** string: ```ts // isFieldAddressValue.ts export const addressSchema = z.object({ addressStreet1: z.string(), // ← required non-null addressStreet2: z.string().nullable(), ... }); ``` …but the backend composite type marks it `isRequired: false` (`address.composite-type.ts`), and the DB column is nullable. So the API legitimately returns `addressStreet1: null` when only other subfields are filled. The two views diverge on how they render: - **Record detail** gates the value behind `useIsFieldEmpty()` → `isFieldValueEmpty()`, which for addresses calls `isFieldAddressValue()`. With `addressStreet1: null` the `safeParse` **fails**, so `isFieldValueEmpty` returns `true` and the `"Empty"` placeholder is shown (`RecordInlineCellDisplayMode`). - **Table view** (`RecordTableCellDisplayMode`) renders `AddressFieldDisplay` directly with **no** empty check, so the address stays visible. This was a latent mismatch since the address guard was introduced. ### Fix Make `addressStreet1` nullable to match the backend and the other subfields: - `addressSchema` → `addressStreet1: z.string().nullable()` - `FieldAddressValue.addressStreet1` → `string | null` - `FieldAddressDraftValue.addressStreet1` → `string | null` (keeps the input/draft type consistent; the text input already renders `?? ''`) The change is strictly more permissive — persisting and the settings default-value form still accept string values; they now also accept `null`. ### Tests - `isFieldAddressValue.test.ts` — guard returns `true` for `addressStreet1: null` with other subfields filled. - `isFieldValueEmpty.test.ts` — new address coverage: empty address is empty; **`street1: null` + city filled is NOT empty**; normal address is not empty. (Added an `addressFieldDefinition` mock.) Both new assertions were confirmed to **fail before the fix** and pass after. ### Verification - `npx jest isFieldValueEmpty isFieldAddressValue normalize-address-field-value-for-persist` → 17 passed - `npx nx typecheck twenty-front` → pass - `npx nx lint:diff-with-main twenty-front` → 0 warnings, 0 errors
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
+2
-2
@@ -4,7 +4,7 @@ import { useArrayField } from '@/object-record/record-field/ui/meta-types/hooks/
|
||||
import { ArrayFieldMenuItem } from '@/object-record/record-field/ui/meta-types/input/components/ArrayFieldMenuItem';
|
||||
import { MultiItemFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/MultiItemFieldInput';
|
||||
import { MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX } from '@/object-record/record-field/ui/meta-types/input/constants/MultiItemFieldInputDropdownClickOutsideId';
|
||||
import { arraySchema } from '@/object-record/record-field/ui/types/guards/isFieldArrayValue';
|
||||
import { arrayFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/arrayFieldValueSchema';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -22,7 +22,7 @@ export const ArrayFieldInput = () => {
|
||||
[draftValue],
|
||||
);
|
||||
const parseStringArrayToArrayValue = (arrayItems: string[]) => {
|
||||
const parseResponse = arraySchema.safeParse(arrayItems);
|
||||
const parseResponse = arrayFieldValueSchema.safeParse(arrayItems);
|
||||
|
||||
if (parseResponse.success) {
|
||||
return parseResponse.data;
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { EmailsFieldMenuItem } from '@/object-record/record-field/ui/meta-types/
|
||||
import { MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX } from '@/object-record/record-field/ui/meta-types/input/constants/MultiItemFieldInputDropdownClickOutsideId';
|
||||
import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/ui/states/recordFieldInputIsFieldInErrorComponentState';
|
||||
import { type FieldEmailsValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { emailsSchema } from '@/object-record/record-field/ui/types/guards/isFieldEmailsValue';
|
||||
import { emailsFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/emailsFieldValueSchema';
|
||||
import { emailSchema } from '@/object-record/record-field/ui/validation-schemas/emailSchema';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@@ -41,7 +41,7 @@ export const EmailsFieldInput = () => {
|
||||
additionalEmails: nextAdditionalEmails,
|
||||
};
|
||||
|
||||
const parseResponse = emailsSchema.safeParse(nextValue);
|
||||
const parseResponse = emailsFieldValueSchema.safeParse(nextValue);
|
||||
|
||||
if (parseResponse.success) {
|
||||
return parseResponse.data;
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ import { MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX } from '@/object-record/recor
|
||||
import { uploadMultipleFiles } from '@/object-record/record-field/ui/meta-types/utils/uploadMultipleFiles';
|
||||
import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/ui/states/recordFieldInputIsFieldInErrorComponentState';
|
||||
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { filesSchema } from '@/object-record/record-field/ui/types/guards/isFieldFilesValue';
|
||||
import { filesFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/filesFieldValueSchema';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { filePreviewState } from '@/ui/field/display/states/filePreviewState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
@@ -39,7 +39,7 @@ export const FilesFieldInput = () => {
|
||||
|
||||
const parseFilesArrayToFilesValue = useCallback(
|
||||
(filesArray: FieldFilesValue[]) => {
|
||||
const parseResponse = filesSchema.safeParse(filesArray);
|
||||
const parseResponse = filesFieldValueSchema.safeParse(filesArray);
|
||||
if (parseResponse.success) {
|
||||
return parseResponse.data;
|
||||
}
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import { MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX } from '@/object-record/recor
|
||||
import { getFieldLinkDefinedLinks } from '@/object-record/record-field/ui/meta-types/input/utils/getFieldLinkDefinedLinks';
|
||||
import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/ui/states/recordFieldInputIsFieldInErrorComponentState';
|
||||
import { type FieldLinksValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { linksSchema } from '@/object-record/record-field/ui/types/guards/isFieldLinksValue';
|
||||
import { linksFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/linksFieldValueSchema';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES } from 'twenty-shared/constants';
|
||||
@@ -38,7 +38,7 @@ export const LinksFieldInput = () => {
|
||||
primaryLinkLabel: nextPrimaryLink?.label ?? null,
|
||||
secondaryLinks: nextSecondaryLinks,
|
||||
};
|
||||
const parseResponse = linksSchema.safeParse(nextValue);
|
||||
const parseResponse = linksFieldValueSchema.safeParse(nextValue);
|
||||
if (parseResponse.success) {
|
||||
return parseResponse.data;
|
||||
}
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ import {
|
||||
type FieldPhonesValue,
|
||||
type PhoneRecord,
|
||||
} from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { phonesSchema } from '@/object-record/record-field/ui/types/guards/isFieldPhonesValue';
|
||||
import { phonesFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/phonesFieldValueSchema';
|
||||
import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton';
|
||||
import { useContext } from 'react';
|
||||
import { MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES } from 'twenty-shared/constants';
|
||||
@@ -103,7 +103,7 @@ export const PhonesFieldInput = () => {
|
||||
primaryPhoneCallingCode: nextPrimaryPhone?.callingCode ?? '',
|
||||
additionalPhones: nextAdditionalPhones,
|
||||
};
|
||||
const parseResponse = phonesSchema.safeParse(nextValue);
|
||||
const parseResponse = phonesFieldValueSchema.safeParse(nextValue);
|
||||
if (parseResponse.success) {
|
||||
return parseResponse.data;
|
||||
}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ export type FieldCurrencyDraftValue = {
|
||||
};
|
||||
export type FieldFullNameDraftValue = { firstName: string; lastName: string };
|
||||
export type FieldAddressDraftValue = {
|
||||
addressStreet1: string;
|
||||
addressStreet1: string | null;
|
||||
addressStreet2: string | null;
|
||||
addressCity: string | null;
|
||||
addressState: string | null;
|
||||
|
||||
+1
-1
@@ -256,7 +256,7 @@ export type FormFieldCurrencyValue = {
|
||||
};
|
||||
export type FieldFullNameValue = { firstName: string; lastName: string };
|
||||
export type FieldAddressValue = {
|
||||
addressStreet1: string;
|
||||
addressStreet1: string | null;
|
||||
addressStreet2: string | null;
|
||||
addressCity: string | null;
|
||||
addressState: string | null;
|
||||
|
||||
+16
-21
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
isFieldAddressValue,
|
||||
addressSchema,
|
||||
} from '@/object-record/record-field/ui/types/guards/isFieldAddressValue';
|
||||
import { isFieldAddressValue } from '@/object-record/record-field/ui/types/guards/isFieldAddressValue';
|
||||
|
||||
describe('isFieldAddressValue', () => {
|
||||
it('should return true for valid address values', () => {
|
||||
@@ -34,6 +31,21 @@ describe('isFieldAddressValue', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when addressStreet1 is null but other subfields are filled', () => {
|
||||
expect(
|
||||
isFieldAddressValue({
|
||||
addressStreet1: null,
|
||||
addressStreet2: null,
|
||||
addressCity: 'Mountain View',
|
||||
addressState: null,
|
||||
addressPostcode: null,
|
||||
addressCountry: null,
|
||||
addressLat: null,
|
||||
addressLng: null,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for incomplete address', () => {
|
||||
expect(isFieldAddressValue({ addressStreet1: '123 Main St' })).toBe(false);
|
||||
});
|
||||
@@ -43,20 +55,3 @@ describe('isFieldAddressValue', () => {
|
||||
expect(isFieldAddressValue('address')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addressSchema', () => {
|
||||
it('should parse a valid address', () => {
|
||||
const result = addressSchema.safeParse({
|
||||
addressStreet1: '123 Main St',
|
||||
addressStreet2: null,
|
||||
addressCity: 'Paris',
|
||||
addressState: null,
|
||||
addressPostcode: '75001',
|
||||
addressCountry: 'France',
|
||||
addressLat: null,
|
||||
addressLng: null,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+2
-23
@@ -1,28 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ALLOWED_ADDRESS_SUBFIELDS } from 'twenty-shared/types';
|
||||
import { type FieldAddressValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
export const addressSchema = z.object({
|
||||
addressStreet1: z.string(),
|
||||
addressStreet2: z.string().nullable(),
|
||||
addressCity: z.string().nullable(),
|
||||
addressState: z.string().nullable(),
|
||||
addressPostcode: z.string().nullable(),
|
||||
addressCountry: z.string().nullable(),
|
||||
addressLat: z.number().nullable(),
|
||||
addressLng: z.number().nullable(),
|
||||
});
|
||||
import { addressFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/addressFieldValueSchema';
|
||||
|
||||
export const isFieldAddressValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldAddressValue =>
|
||||
addressSchema.safeParse(fieldValue).success;
|
||||
|
||||
export const addressSettingsSchema = z.object({
|
||||
subFields: z
|
||||
.array(z.enum(ALLOWED_ADDRESS_SUBFIELDS))
|
||||
.min(1)
|
||||
.optional()
|
||||
.nullable(),
|
||||
});
|
||||
addressFieldValueSchema.safeParse(fieldValue).success;
|
||||
|
||||
+3
-4
@@ -1,8 +1,7 @@
|
||||
import { type FieldArrayValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const arraySchema = z.union([z.null(), z.array(z.string())]);
|
||||
import { arrayFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/arrayFieldValueSchema';
|
||||
|
||||
export const isFieldArrayValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldArrayValue => arraySchema.safeParse(fieldValue).success;
|
||||
): fieldValue is FieldArrayValue =>
|
||||
arrayFieldValueSchema.safeParse(fieldValue).success;
|
||||
|
||||
+2
-9
@@ -1,14 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CurrencyCode } from 'twenty-shared/constants';
|
||||
import { type FieldCurrencyValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
const currencySchema = z.object({
|
||||
currencyCode: z.union([z.enum(CurrencyCode), z.literal('')]).nullable(),
|
||||
amountMicros: z.number().nullable(),
|
||||
});
|
||||
import { currencyFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/currencyFieldValueSchema';
|
||||
|
||||
export const isFieldCurrencyValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldCurrencyValue =>
|
||||
currencySchema.safeParse(fieldValue).success;
|
||||
currencyFieldValueSchema.safeParse(fieldValue).success;
|
||||
|
||||
+3
-8
@@ -1,12 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type FieldEmailsValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
export const emailsSchema = z.object({
|
||||
primaryEmail: z.string(),
|
||||
additionalEmails: z.array(z.string()).nullable(),
|
||||
}) satisfies z.ZodType<FieldEmailsValue>;
|
||||
import { emailsFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/emailsFieldValueSchema';
|
||||
|
||||
export const isFieldEmailsValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldEmailsValue => emailsSchema.safeParse(fieldValue).success;
|
||||
): fieldValue is FieldEmailsValue =>
|
||||
emailsFieldValueSchema.safeParse(fieldValue).success;
|
||||
|
||||
+3
-22
@@ -1,26 +1,7 @@
|
||||
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { z } from 'zod';
|
||||
import { filesFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/filesFieldValueSchema';
|
||||
|
||||
const fileSchema = z.object({
|
||||
fileId: z.string(),
|
||||
label: z.string(),
|
||||
extension: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
fileCategory: z
|
||||
.enum([
|
||||
'ARCHIVE',
|
||||
'AUDIO',
|
||||
'IMAGE',
|
||||
'PRESENTATION',
|
||||
'SPREADSHEET',
|
||||
'TEXT_DOCUMENT',
|
||||
'VIDEO',
|
||||
'OTHER',
|
||||
] as const)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const filesSchema = z.array(fileSchema);
|
||||
export const isFieldFilesValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldFilesValue[] => filesSchema.safeParse(fieldValue).success;
|
||||
): fieldValue is FieldFilesValue[] =>
|
||||
filesFieldValueSchema.safeParse(fieldValue).success;
|
||||
|
||||
+2
-8
@@ -1,13 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type FieldFullNameValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
const fullnameSchema = z.object({
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
});
|
||||
import { fullNameFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/fullNameFieldValueSchema';
|
||||
|
||||
export const isFieldFullNameValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldFullNameValue =>
|
||||
fullnameSchema.safeParse(fieldValue).success;
|
||||
fullNameFieldValueSchema.safeParse(fieldValue).success;
|
||||
|
||||
+3
-15
@@ -1,19 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import { type FieldLinksValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
export const linksSchema = z.object({
|
||||
primaryLinkLabel: z.string().nullable(),
|
||||
primaryLinkUrl: z.string().nullable(),
|
||||
secondaryLinks: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string().nullable(),
|
||||
url: z.string().nullable(),
|
||||
}),
|
||||
)
|
||||
.nullable(),
|
||||
}) satisfies z.ZodType<FieldLinksValue>;
|
||||
import { linksFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/linksFieldValueSchema';
|
||||
|
||||
export const isFieldLinksValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldLinksValue => linksSchema.safeParse(fieldValue).success;
|
||||
): fieldValue is FieldLinksValue =>
|
||||
linksFieldValueSchema.safeParse(fieldValue).success;
|
||||
|
||||
+3
-18
@@ -1,22 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type FieldPhonesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
export const phonesSchema = z.object({
|
||||
primaryPhoneNumber: z.string(),
|
||||
primaryPhoneCountryCode: z.string(),
|
||||
primaryPhoneCallingCode: z.string(),
|
||||
additionalPhones: z
|
||||
.array(
|
||||
z.object({
|
||||
number: z.string(),
|
||||
callingCode: z.string(),
|
||||
countryCode: z.string(),
|
||||
}),
|
||||
)
|
||||
.nullable(),
|
||||
}) satisfies z.ZodType<FieldPhonesValue>;
|
||||
import { phonesFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/phonesFieldValueSchema';
|
||||
|
||||
export const isFieldPhonesValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldPhonesValue => phonesSchema.safeParse(fieldValue).success;
|
||||
): fieldValue is FieldPhonesValue =>
|
||||
phonesFieldValueSchema.safeParse(fieldValue).success;
|
||||
|
||||
+3
-23
@@ -1,27 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type FieldJsonValue,
|
||||
type Json,
|
||||
} from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
// See https://zod.dev/?id=json-type
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||
const jsonSchema: z.ZodType<Json> = z.lazy(() =>
|
||||
z.union([
|
||||
literalSchema,
|
||||
z.array(jsonSchema),
|
||||
z.record(z.string(), jsonSchema),
|
||||
]),
|
||||
);
|
||||
|
||||
export const jsonWithoutLiteralsSchema: z.ZodType<FieldJsonValue> = z.union([
|
||||
z.null(), // Exclude literal values other than null
|
||||
z.array(jsonSchema),
|
||||
z.record(z.string(), jsonSchema),
|
||||
]);
|
||||
import { type FieldJsonValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { rawJsonFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/rawJsonFieldValueSchema';
|
||||
|
||||
export const isFieldRawJsonValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldJsonValue =>
|
||||
jsonWithoutLiteralsSchema.safeParse(fieldValue).success;
|
||||
rawJsonFieldValueSchema.safeParse(fieldValue).success;
|
||||
|
||||
+2
-7
@@ -1,12 +1,7 @@
|
||||
import { type FieldRichTextValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const richTextSchema: z.ZodType<FieldRichTextValue> = z.object({
|
||||
blocknote: z.string().nullable(),
|
||||
markdown: z.string().nullable(),
|
||||
});
|
||||
import { richTextFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/richTextFieldValueSchema';
|
||||
|
||||
export const isFieldRichTextValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldRichTextValue =>
|
||||
richTextSchema.safeParse(fieldValue).success;
|
||||
richTextFieldValueSchema.safeParse(fieldValue).success;
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { ALLOWED_ADDRESS_SUBFIELDS } from 'twenty-shared/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const addressFieldSettingsSchema = z.object({
|
||||
subFields: z
|
||||
.array(z.enum(ALLOWED_ADDRESS_SUBFIELDS))
|
||||
.min(1)
|
||||
.optional()
|
||||
.nullable(),
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const addressFieldValueSchema = z.object({
|
||||
addressStreet1: z.string().nullable(),
|
||||
addressStreet2: z.string().nullable(),
|
||||
addressCity: z.string().nullable(),
|
||||
addressState: z.string().nullable(),
|
||||
addressPostcode: z.string().nullable(),
|
||||
addressCountry: z.string().nullable(),
|
||||
addressLat: z.number().nullable(),
|
||||
addressLng: z.number().nullable(),
|
||||
});
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const arrayFieldValueSchema = z.union([z.null(), z.array(z.string())]);
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { CurrencyCode } from 'twenty-shared/constants';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const currencyFieldValueSchema = z.object({
|
||||
currencyCode: z.union([z.enum(CurrencyCode), z.literal('')]).nullable(),
|
||||
amountMicros: z.number().nullable(),
|
||||
});
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type FieldEmailsValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
export const emailsFieldValueSchema = z.object({
|
||||
primaryEmail: z.string(),
|
||||
additionalEmails: z.array(z.string()).nullable(),
|
||||
}) satisfies z.ZodType<FieldEmailsValue>;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const fileSchema = z.object({
|
||||
fileId: z.string(),
|
||||
label: z.string(),
|
||||
extension: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
fileCategory: z
|
||||
.enum([
|
||||
'ARCHIVE',
|
||||
'AUDIO',
|
||||
'IMAGE',
|
||||
'PRESENTATION',
|
||||
'SPREADSHEET',
|
||||
'TEXT_DOCUMENT',
|
||||
'VIDEO',
|
||||
'OTHER',
|
||||
] as const)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const filesFieldValueSchema = z.array(fileSchema);
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const fullNameFieldValueSchema = z.object({
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
});
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type FieldLinksValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
export const linksFieldValueSchema = z.object({
|
||||
primaryLinkLabel: z.string().nullable(),
|
||||
primaryLinkUrl: z.string().nullable(),
|
||||
secondaryLinks: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string().nullable(),
|
||||
url: z.string().nullable(),
|
||||
}),
|
||||
)
|
||||
.nullable(),
|
||||
}) satisfies z.ZodType<FieldLinksValue>;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type FieldPhonesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
export const phonesFieldValueSchema = z.object({
|
||||
primaryPhoneNumber: z.string(),
|
||||
primaryPhoneCountryCode: z.string(),
|
||||
primaryPhoneCallingCode: z.string().optional(),
|
||||
additionalPhones: z
|
||||
.array(
|
||||
z.object({
|
||||
number: z.string(),
|
||||
callingCode: z.string(),
|
||||
countryCode: z.string(),
|
||||
}),
|
||||
)
|
||||
.nullable(),
|
||||
}) satisfies z.ZodType<FieldPhonesValue>;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type FieldJsonValue,
|
||||
type Json,
|
||||
} from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
// See https://zod.dev/?id=json-type
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||
const jsonSchema: z.ZodType<Json> = z.lazy(() =>
|
||||
z.union([
|
||||
literalSchema,
|
||||
z.array(jsonSchema),
|
||||
z.record(z.string(), jsonSchema),
|
||||
]),
|
||||
);
|
||||
|
||||
export const rawJsonFieldValueSchema: z.ZodType<FieldJsonValue> = z.union([
|
||||
z.null(), // Exclude literal values other than null
|
||||
z.array(jsonSchema),
|
||||
z.record(z.string(), jsonSchema),
|
||||
]);
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type FieldRichTextValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
export const richTextFieldValueSchema = z.object({
|
||||
blocknote: z.string().nullable(),
|
||||
markdown: z.string().nullable(),
|
||||
}) satisfies z.ZodType<FieldRichTextValue>;
|
||||
+4
-6
@@ -1,10 +1,8 @@
|
||||
import { Separator } from '@/settings/components/Separator';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import {
|
||||
addressSchema as addressFieldDefaultValueSchema,
|
||||
addressSettingsSchema,
|
||||
} from '@/object-record/record-field/ui/types/guards/isFieldAddressValue';
|
||||
import { addressFieldSettingsSchema } from '@/object-record/record-field/ui/validation-schemas/addressFieldSettingsSchema';
|
||||
import { addressFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/addressFieldValueSchema';
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { MultiSelectAddressFields } from '@/settings/data-model/fields/forms/address/components/MultiSelectAddressFields';
|
||||
import { DEFAULT_SELECTION_ADDRESS_WITH_MESSAGES } from '@/settings/data-model/fields/forms/address/constants/DefaultSelectionAddressWithMessages';
|
||||
@@ -33,8 +31,8 @@ type SettingsDataModelFieldAddressFormProps = {
|
||||
};
|
||||
|
||||
export const settingsDataModelFieldAddressFormSchema = z.object({
|
||||
defaultValue: addressFieldDefaultValueSchema,
|
||||
settings: addressSettingsSchema,
|
||||
defaultValue: addressFieldValueSchema,
|
||||
settings: addressFieldSettingsSchema,
|
||||
});
|
||||
|
||||
export type SettingsDataModelFieldTextFormValues = z.infer<
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { phonesSchema as phonesFieldDefaultValueSchema } from '@/object-record/record-field/ui/types/guards/isFieldPhonesValue';
|
||||
import { phonesFieldValueSchema } from '@/object-record/record-field/ui/validation-schemas/phonesFieldValueSchema';
|
||||
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: phonesFieldDefaultValueSchema,
|
||||
defaultValue: phonesFieldValueSchema,
|
||||
})
|
||||
.merge(
|
||||
mergeSettingsSchemas(
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
Reference in New Issue
Block a user