diff --git a/customer-insights.png b/customer-insights.png new file mode 100644 index 00000000000..7f5fdf0d9c7 Binary files /dev/null and b/customer-insights.png differ diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/ArrayFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/ArrayFieldInput.tsx index 92f388984f5..f139f8e3c41 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/ArrayFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/ArrayFieldInput.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/EmailsFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/EmailsFieldInput.tsx index a193b4bbd67..466a21bbe43 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/EmailsFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/EmailsFieldInput.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/FilesFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/FilesFieldInput.tsx index 39bc8f80979..c1623248b75 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/FilesFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/FilesFieldInput.tsx @@ -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; } diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/LinksFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/LinksFieldInput.tsx index 4ff44c5ea6b..f195afab4ab 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/LinksFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/LinksFieldInput.tsx @@ -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; } diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/PhonesFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/PhonesFieldInput.tsx index 9a4f44513d7..3101a6cf75d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/PhonesFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/PhonesFieldInput.tsx @@ -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; } diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldInputDraftValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldInputDraftValue.ts index f40baedab82..e504d62b43d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldInputDraftValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldInputDraftValue.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts index 2971d49228a..8b0f7dfa702 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/__tests__/isFieldAddressValue.test.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/__tests__/isFieldAddressValue.test.ts index 9e0e1e891ea..c80193917ba 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/__tests__/isFieldAddressValue.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/__tests__/isFieldAddressValue.test.ts @@ -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); - }); -}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldAddressValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldAddressValue.ts index 157ba39f6c0..618b644ca44 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldAddressValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldAddressValue.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldArrayValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldArrayValue.ts index c733e139d4e..d65d43827cc 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldArrayValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldArrayValue.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldCurrencyValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldCurrencyValue.ts index 4c167512237..a96f0201de4 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldCurrencyValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldCurrencyValue.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldEmailsValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldEmailsValue.ts index 35224c9a343..d25206d4122 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldEmailsValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldEmailsValue.ts @@ -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; +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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldFilesValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldFilesValue.ts index b66a308d3f5..a1907daa883 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldFilesValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldFilesValue.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldFullNameValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldFullNameValue.ts index 05ec7d3f61f..ef72e59b841 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldFullNameValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldFullNameValue.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldLinksValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldLinksValue.ts index 43b075c9a26..29e8c29ed8d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldLinksValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldLinksValue.ts @@ -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; +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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldPhonesValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldPhonesValue.ts index ed883bfd6b4..13acd8f5049 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldPhonesValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldPhonesValue.ts @@ -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; +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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRawJsonValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRawJsonValue.ts index c2940fff082..ff20095a191 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRawJsonValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRawJsonValue.ts @@ -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 = z.lazy(() => - z.union([ - literalSchema, - z.array(jsonSchema), - z.record(z.string(), jsonSchema), - ]), -); - -export const jsonWithoutLiteralsSchema: z.ZodType = 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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRichTextValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRichTextValue.ts index bb19b9f986e..dc731b039c5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRichTextValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRichTextValue.ts @@ -1,12 +1,7 @@ import { type FieldRichTextValue } from '@/object-record/record-field/ui/types/FieldMetadata'; -import { z } from 'zod'; - -export const richTextSchema: z.ZodType = 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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/addressFieldSettingsSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/addressFieldSettingsSchema.ts new file mode 100644 index 00000000000..fadf2825097 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/addressFieldSettingsSchema.ts @@ -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(), +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/addressFieldValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/addressFieldValueSchema.ts new file mode 100644 index 00000000000..a94d3725cfd --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/addressFieldValueSchema.ts @@ -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(), +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/arrayFieldValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/arrayFieldValueSchema.ts new file mode 100644 index 00000000000..681212e5dda --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/arrayFieldValueSchema.ts @@ -0,0 +1,3 @@ +import { z } from 'zod'; + +export const arrayFieldValueSchema = z.union([z.null(), z.array(z.string())]); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/currencyFieldValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/currencyFieldValueSchema.ts new file mode 100644 index 00000000000..948960f21e6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/currencyFieldValueSchema.ts @@ -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(), +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/emailsFieldValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/emailsFieldValueSchema.ts new file mode 100644 index 00000000000..a9ae538cca3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/emailsFieldValueSchema.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/filesFieldValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/filesFieldValueSchema.ts new file mode 100644 index 00000000000..f582f155099 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/filesFieldValueSchema.ts @@ -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); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/fullNameFieldValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/fullNameFieldValueSchema.ts new file mode 100644 index 00000000000..4afb99ea0cf --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/fullNameFieldValueSchema.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; + +export const fullNameFieldValueSchema = z.object({ + firstName: z.string(), + lastName: z.string(), +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/linksFieldValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/linksFieldValueSchema.ts new file mode 100644 index 00000000000..2817daa7f48 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/linksFieldValueSchema.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/phonesFieldValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/phonesFieldValueSchema.ts new file mode 100644 index 00000000000..3f6bd173c2d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/phonesFieldValueSchema.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/rawJsonFieldValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/rawJsonFieldValueSchema.ts new file mode 100644 index 00000000000..df129d5f189 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/rawJsonFieldValueSchema.ts @@ -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 = z.lazy(() => + z.union([ + literalSchema, + z.array(jsonSchema), + z.record(z.string(), jsonSchema), + ]), +); + +export const rawJsonFieldValueSchema: z.ZodType = z.union([ + z.null(), // Exclude literal values other than null + z.array(jsonSchema), + z.record(z.string(), jsonSchema), +]); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/richTextFieldValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/richTextFieldValueSchema.ts new file mode 100644 index 00000000000..fe6745d3d2d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/richTextFieldValueSchema.ts @@ -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; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm.tsx index 2d83d2074a4..c276b25e367 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm.tsx @@ -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< 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 a6b197f986b..67112ce36bd 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 { 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( diff --git a/sales-overview.png b/sales-overview.png new file mode 100644 index 00000000000..29156d3f2df Binary files /dev/null and b/sales-overview.png differ