From c7d61e183a691354774e651d2e47cbef8913b348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Fri, 24 May 2024 18:06:57 +0200 Subject: [PATCH] feat: simplify field preview logic in Settings (#5541) Closes #5382 TODO: - [x] Test all field previews in app - [x] Fix tests - [x] Fix JSON preview --- package.json | 2 +- .../twenty-chrome-extension/src/manifest.ts | 2 +- .../record-field/utils/isFieldValueEmpty.ts | 10 +- .../currencyFieldDefaultValueSchema.ts | 15 ++ .../multiSelectFieldDefaultValueSchema.ts | 26 +++ .../selectFieldDefaultValueSchema.ts | 22 ++ .../constants/SettingsFieldTypeConfigs.ts | 12 +- ...SettingsDataModelFieldSettingsFormCard.tsx | 17 +- .../SettingsDataModelFieldCurrencyForm.tsx | 14 +- .../SettingsDataModelFieldSelectForm.tsx | 7 +- .../SettingsDataModelFieldPreview.tsx | 39 +++- ...tingsDataModelFieldPreviewCard.stories.tsx | 70 ++++++- .../hooks/__tests__/useFieldPreview.test.tsx | 67 ------- .../__tests__/useFieldPreviewValue.test.tsx | 189 ++++++++++++++++++ .../fields/preview/hooks/useFieldPreview.ts | 106 ---------- .../preview/hooks/useFieldPreviewValue.ts | 51 +++++ .../fields/preview/hooks/usePreviewRecord.ts | 65 ++++++ .../hooks/useRelationFieldPreviewValue.ts | 22 ++ .../getCurrencyFieldPreviewValue.test.ts | 126 ++++++++++++ .../__tests__/getFieldPreviewValue.test.ts | 85 ++++++++ .../getMultiSelectFieldPreviewValue.test.ts | 129 ++++++++++++ .../getSelectFieldPreviewValue.test.ts | 124 ++++++++++++ .../utils/getCurrencyFieldPreviewValue.ts | 32 +++ .../preview/utils/getFieldPreviewValue.ts | 34 ++++ .../utils/getMultiSelectFieldPreviewValue.ts | 36 ++++ .../utils/getSelectFieldPreviewValue.ts | 33 +++ .../getFieldDefaultPreviewValue.test.ts | 176 ---------------- .../utils/getFieldDefaultPreviewValue.ts | 89 --------- .../utils/getSettingsFieldTypeConfig.ts | 9 +- .../standard-metadata-query-result.ts | 29 +-- .../src/testing/mock-data/metadata.ts | 43 ++++ packages/twenty-front/src/utils/date-utils.ts | 2 + yarn.lock | 11 +- 33 files changed, 1184 insertions(+), 510 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/validation-schemas/multiSelectFieldDefaultValueSchema.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/validation-schemas/selectFieldDefaultValueSchema.ts delete mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreview.test.tsx create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx delete mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreview.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/usePreviewRecord.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useRelationFieldPreviewValue.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getCurrencyFieldPreviewValue.test.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getMultiSelectFieldPreviewValue.test.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getSelectFieldPreviewValue.test.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue.ts delete mode 100644 packages/twenty-front/src/modules/settings/data-model/utils/__tests__/getFieldDefaultPreviewValue.test.ts delete mode 100644 packages/twenty-front/src/modules/settings/data-model/utils/getFieldDefaultPreviewValue.ts diff --git a/package.json b/package.json index d6c4d06258..c8786eca8b 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "uuid": "^9.0.0", "vite-tsconfig-paths": "^4.2.1", "xlsx-ugnis": "^0.19.3", - "zod": "^3.22.2" + "zod": "3.23.8" }, "devDependencies": { "@babel/core": "^7.14.5", diff --git a/packages/twenty-chrome-extension/src/manifest.ts b/packages/twenty-chrome-extension/src/manifest.ts index 39b15c790f..b48422207a 100644 --- a/packages/twenty-chrome-extension/src/manifest.ts +++ b/packages/twenty-chrome-extension/src/manifest.ts @@ -46,7 +46,7 @@ export default defineManifest({ permissions: ['activeTab', 'storage', 'identity', 'sidePanel', 'cookies'], - // setting host permissions to all http connections will allow + // setting host permissions to all http connections will allow // for people who host on their custom domain to get access to // extension instead of white listing individual urls host_permissions: ['https://*/*', 'http://*/*'], diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts index 6c1ad7f1f4..5bea46d431 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts @@ -1,3 +1,5 @@ +import { isString } from '@sniptt/guards'; + import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; @@ -26,8 +28,11 @@ import { isFieldSelectValue } from '@/object-record/record-field/types/guards/is import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid'; import { isDefined } from '~/utils/isDefined'; +import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; -const isValueEmpty = (value: unknown) => !isDefined(value) || value === ''; +const isValueEmpty = (value: unknown) => + !isDefined(value) || + (isString(value) && stripSimpleQuotesFromString(value) === ''); export const isFieldValueEmpty = ({ fieldDefinition, @@ -78,7 +83,8 @@ export const isFieldValueEmpty = ({ if (isFieldFullName(fieldDefinition)) { return ( !isFieldFullNameValue(fieldValue) || - isValueEmpty(fieldValue?.firstName + fieldValue?.lastName) + (isValueEmpty(fieldValue?.firstName) && + isValueEmpty(fieldValue?.lastName)) ); } diff --git a/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema.ts new file mode 100644 index 0000000000..0f007db346 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; +import { currencyCodeSchema } from '@/object-record/record-field/validation-schemas/currencyCodeSchema'; +import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; +import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema'; + +export const currencyFieldDefaultValueSchema = z.object({ + amountMicros: z.number().nullable(), + currencyCode: simpleQuotesStringSchema.refine( + (value): value is `'${CurrencyCode}'` => + currencyCodeSchema.safeParse(stripSimpleQuotesFromString(value)).success, + { message: 'String is not a valid currencyCode' }, + ), +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/multiSelectFieldDefaultValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/multiSelectFieldDefaultValueSchema.ts new file mode 100644 index 0000000000..855e7c1ee1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/multiSelectFieldDefaultValueSchema.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; + +import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; +import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; +import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema'; + +export const multiSelectFieldDefaultValueSchema = ( + options?: FieldMetadataItemOption[], +) => { + if (!options?.length) return z.array(simpleQuotesStringSchema).nullable(); + + const optionValues = options.map(({ value }) => value); + + return z + .array( + simpleQuotesStringSchema.refine( + (value) => optionValues.includes(stripSimpleQuotesFromString(value)), + { + message: `String is not a valid multi-select option, available options are: ${options.join( + ', ', + )}`, + }, + ), + ) + .nullable(); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/selectFieldDefaultValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/selectFieldDefaultValueSchema.ts new file mode 100644 index 0000000000..f4623acc8d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/selectFieldDefaultValueSchema.ts @@ -0,0 +1,22 @@ +import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; +import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; +import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema'; + +export const selectFieldDefaultValueSchema = ( + options?: FieldMetadataItemOption[], +) => { + if (!options?.length) return simpleQuotesStringSchema.nullable(); + + const optionValues = options.map(({ value }) => value); + + return simpleQuotesStringSchema + .refine( + (value) => optionValues.includes(stripSimpleQuotesFromString(value)), + { + message: `String is not a valid select option, available options are: ${options.join( + ', ', + )}`, + }, + ) + .nullable(); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts index 9429baaf53..681062d937 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts @@ -32,10 +32,7 @@ export type SettingsFieldTypeConfig = { defaultValue?: unknown; }; -export const SETTINGS_FIELD_TYPE_CONFIGS: Record< - SettingsSupportedFieldType, - SettingsFieldTypeConfig -> = { +export const SETTINGS_FIELD_TYPE_CONFIGS = { [FieldMetadataType.Uuid]: { label: 'Unique ID', Icon: IconKey, @@ -137,6 +134,9 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record< [FieldMetadataType.RawJson]: { label: 'JSON', Icon: IconJson, - defaultValue: `{ "key": "value" }`, + defaultValue: { key: 'value' }, }, -}; +} as const satisfies Record< + SettingsSupportedFieldType, + SettingsFieldTypeConfig +>; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx index a81da911e2..d0e6604d14 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx @@ -80,21 +80,22 @@ const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` `; const previewableTypes = [ + FieldMetadataType.Address, FieldMetadataType.Boolean, FieldMetadataType.Currency, - FieldMetadataType.DateTime, FieldMetadataType.Date, - FieldMetadataType.Select, - FieldMetadataType.MultiSelect, + FieldMetadataType.DateTime, + FieldMetadataType.FullName, FieldMetadataType.Link, FieldMetadataType.Links, + FieldMetadataType.MultiSelect, FieldMetadataType.Number, - FieldMetadataType.Rating, - FieldMetadataType.Relation, - FieldMetadataType.Text, - FieldMetadataType.Address, - FieldMetadataType.RawJson, FieldMetadataType.Phone, + FieldMetadataType.Rating, + FieldMetadataType.RawJson, + FieldMetadataType.Relation, + FieldMetadataType.Select, + FieldMetadataType.Text, ]; export const SettingsDataModelFieldSettingsFormCard = ({ diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx index c8711cbab0..52b0a4d010 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx @@ -2,25 +2,15 @@ import { Controller, useFormContext } from 'react-hook-form'; import { z } from 'zod'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { currencyCodeSchema } from '@/object-record/record-field/validation-schemas/currencyCodeSchema'; +import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema'; import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes'; import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues'; import { Select } from '@/ui/input/components/Select'; import { CardContent } from '@/ui/layout/card/components/CardContent'; import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString'; -import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; -import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema'; export const settingsDataModelFieldCurrencyFormSchema = z.object({ - defaultValue: z.object({ - amountMicros: z.number().nullable(), - currencyCode: simpleQuotesStringSchema.refine( - (value) => - currencyCodeSchema.safeParse(stripSimpleQuotesFromString(value)) - .success, - { message: 'String is not a valid currencyCode' }, - ), - }), + defaultValue: currencyFieldDefaultValueSchema, }); export type SettingsDataModelFieldCurrencyFormValues = z.infer< diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx index a405b34494..0f30a88b6c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx @@ -9,6 +9,8 @@ import { FieldMetadataItemOption, } from '@/object-metadata/types/FieldMetadataItem'; import { selectOptionsSchema } from '@/object-metadata/validation-schemas/selectOptionsSchema'; +import { multiSelectFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/multiSelectFieldDefaultValueSchema'; +import { selectFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/selectFieldDefaultValueSchema'; import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues'; import { generateNewSelectOption } from '@/settings/data-model/fields/forms/select/utils/generateNewSelectOption'; import { isSelectOptionDefaultValue } from '@/settings/data-model/utils/isSelectOptionDefaultValue'; @@ -21,17 +23,16 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; import { moveArrayItem } from '~/utils/array/moveArrayItem'; import { toSpliced } from '~/utils/array/toSpliced'; import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString'; -import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema'; import { SettingsDataModelFieldSelectFormOptionRow } from './SettingsDataModelFieldSelectFormOptionRow'; export const settingsDataModelFieldSelectFormSchema = z.object({ - defaultValue: simpleQuotesStringSchema.nullable(), + defaultValue: selectFieldDefaultValueSchema(), options: selectOptionsSchema, }); export const settingsDataModelFieldMultiSelectFormSchema = z.object({ - defaultValue: z.array(simpleQuotesStringSchema).nullable(), + defaultValue: multiSelectFieldDefaultValueSchema(), options: selectOptionsSchema, }); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx index f34ecd5e3b..2d44fe62f2 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx @@ -4,13 +4,15 @@ import { useIcons } from 'twenty-ui'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { BooleanFieldInput } from '@/object-record/record-field/meta-types/input/components/BooleanFieldInput'; import { RatingFieldInput } from '@/object-record/record-field/meta-types/input/components/RatingFieldInput'; import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect'; import { SettingsDataModelSetRecordEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect'; -import { useFieldPreview } from '@/settings/data-model/fields/preview/hooks/useFieldPreview'; +import { useFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useFieldPreviewValue'; +import { usePreviewRecord } from '@/settings/data-model/fields/preview/hooks/usePreviewRecord'; import { FieldMetadataType } from '~/generated-metadata/graphql'; export type SettingsDataModelFieldPreviewProps = { @@ -61,17 +63,40 @@ export const SettingsDataModelFieldPreview = ({ const { getIcon } = useIcons(); const FieldIcon = getIcon(fieldMetadataItem.icon); - const { entityId, fieldName, fieldPreviewValue, isLabelIdentifier, record } = - useFieldPreview({ - fieldMetadataItem, + // id and name are undefined in create mode (field does not exist yet) + // and defined in edit mode. + const isLabelIdentifier = + !!fieldMetadataItem.id && + !!fieldMetadataItem.name && + isLabelIdentifierField({ + fieldMetadataItem: { + id: fieldMetadataItem.id, + name: fieldMetadataItem.name, + }, objectMetadataItem, - relationObjectMetadataItem, }); + const previewRecord = usePreviewRecord({ + objectMetadataItem, + skip: !isLabelIdentifier, + }); + + const fieldPreviewValue = useFieldPreviewValue({ + fieldMetadataItem, + relationObjectMetadataItem, + skip: isLabelIdentifier, + }); + + const fieldName = + fieldMetadataItem.name || `${fieldMetadataItem.type}-new-field`; + const entityId = + previewRecord?.id ?? + `${objectMetadataItem.nameSingular}-${fieldName}-preview`; + return ( <> - {record ? ( - + {previewRecord ? ( + ) : ( = { SnackBarDecorator, ], args: { - fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( - ({ type }) => type === FieldMetadataType.Text, - ), - objectMetadataItem: mockedCompanyObjectMetadataItem, + objectMetadataItem: mockedPersonObjectMetadataItem, }, parameters: { container: { width: 480 }, @@ -38,21 +36,41 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Text: Story = {}; +export const LabelIdentifier: Story = { + args: { + fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find( + ({ name, type }) => + name === 'name' && type === FieldMetadataType.FullName, + ), + }, +}; + +export const Text: Story = { + args: { + fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find( + ({ name, type }) => name === 'city' && type === FieldMetadataType.Text, + ), + }, +}; export const Boolean: Story = { args: { fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( - ({ type }) => type === FieldMetadataType.Boolean, + ({ name, type }) => + name === 'idealCustomerProfile' && type === FieldMetadataType.Boolean, ), + objectMetadataItem: mockedCompanyObjectMetadataItem, }, }; export const Currency: Story = { args: { fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( - ({ type }) => type === FieldMetadataType.Currency, + ({ name, type }) => + name === 'annualRecurringRevenue' && + type === FieldMetadataType.Currency, ), + objectMetadataItem: mockedCompanyObjectMetadataItem, }, }; @@ -61,14 +79,27 @@ export const Date: Story = { fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( ({ type }) => type === FieldMetadataType.DateTime, ), + objectMetadataItem: mockedCompanyObjectMetadataItem, }, }; export const Link: Story = { args: { fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( - ({ type }) => type === FieldMetadataType.Link, + ({ name, type }) => + name === 'linkedinLink' && type === FieldMetadataType.Link, ), + objectMetadataItem: mockedCompanyObjectMetadataItem, + }, +}; + +export const Links: Story = { + args: { + ...Link.args, + fieldMetadataItem: { + ...Link.args!.fieldMetadataItem!, + type: FieldMetadataType.Links, + }, }, }; @@ -77,6 +108,7 @@ export const Number: Story = { fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( ({ type }) => type === FieldMetadataType.Number, ), + objectMetadataItem: mockedCompanyObjectMetadataItem, }, }; @@ -95,7 +127,27 @@ export const Relation: Story = { fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find( ({ name }) => name === 'company', ), - objectMetadataItem: mockedPersonObjectMetadataItem, relationObjectMetadataItem: mockedCompanyObjectMetadataItem, }, }; + +export const Select: Story = { + args: { + fieldMetadataItem: mockedOpportunityObjectMetadataItem.fields.find( + ({ name, type }) => name === 'stage' && type === FieldMetadataType.Select, + ), + objectMetadataItem: mockedOpportunityObjectMetadataItem, + }, +}; + +export const MultiSelect: Story = { + args: { + ...Select.args, + fieldMetadataItem: { + ...Select.args!.fieldMetadataItem!, + defaultValue: null, + label: 'Stages', + type: FieldMetadataType.MultiSelect, + }, + }, +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreview.test.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreview.test.tsx deleted file mode 100644 index d0200b2ca2..0000000000 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreview.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; -import { renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; - -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; -import { mockedCompanyObjectMetadataItem } from '~/testing/mock-data/metadata'; - -import { useFieldPreview } from '../useFieldPreview'; - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - - - {children} - - - -); - -describe('useFieldPreview', () => { - it('returns default preview data if no records are found', () => { - // Given - const objectMetadataItem = mockedCompanyObjectMetadataItem; - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( - ({ name }) => name === 'linkedinLink', - )!; - - // When - const { result } = renderHook( - () => useFieldPreview({ fieldMetadataItem, objectMetadataItem }), - { wrapper: Wrapper }, - ); - - // Then - expect(result.current).toEqual({ - entityId: 'company-linkedinLink-preview-field-form', - fieldName: 'linkedinLink', - fieldPreviewValue: { label: '', url: 'www.twenty.com' }, - isLabelIdentifier: false, - record: null, - }); - }); - - it('returns default preview data for a label identifier field if no records are found', () => { - // Given - const objectMetadataItem = mockedCompanyObjectMetadataItem; - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( - ({ name }) => name === 'name', - )!; - - // When - const { result } = renderHook( - () => useFieldPreview({ fieldMetadataItem, objectMetadataItem }), - { wrapper: Wrapper }, - ); - - // Then - expect(result.current).toEqual({ - entityId: 'company-name-preview-field-form', - fieldName: 'name', - fieldPreviewValue: 'Company', - isLabelIdentifier: true, - record: null, - }); - }); -}); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx new file mode 100644 index 0000000000..a3fbadbf1a --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx @@ -0,0 +1,189 @@ +import { ReactNode } from 'react'; +import { MockedProvider } from '@apollo/client/testing'; +import { renderHook } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; + +import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; +import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; +import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; +import { FieldMetadataType } from '~/generated/graphql'; +import { + mockedCompanyObjectMetadataItem, + mockedOpportunityObjectMetadataItem, + mockedPersonObjectMetadataItem, +} from '~/testing/mock-data/metadata'; + +import { useFieldPreviewValue } from '../useFieldPreviewValue'; + +const Wrapper = ({ children }: { children: ReactNode }) => ( + + + + {children} + + + +); + +describe('useFieldPreviewValue', () => { + it('returns null if skip is true', () => { + // Given + const fieldName = 'amount'; + const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( + ({ name, type }) => + name === fieldName && type === FieldMetadataType.Currency, + ); + const skip = true; + + if (!fieldMetadataItem) { + throw new Error(`Field ${fieldName} not found`); + } + + // When + const { result } = renderHook( + () => useFieldPreviewValue({ fieldMetadataItem, skip }), + { wrapper: Wrapper }, + ); + + // Then + expect(result.current).toBeNull(); + }); + + it("returns the field's preview value for a Currency field", () => { + // Given + const fieldName = 'amount'; + const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( + ({ name, type }) => + name === fieldName && type === FieldMetadataType.Currency, + ); + + if (!fieldMetadataItem) { + throw new Error(`Field ${fieldName} not found`); + } + + // When + const { result } = renderHook( + () => useFieldPreviewValue({ fieldMetadataItem }), + { wrapper: Wrapper }, + ); + + // Then + expect(result.current).toEqual({ + amountMicros: 2000000000, + currencyCode: 'USD', + }); + }); + + it("returns the relation object's label identifier preview value for a Relation field", () => { + // Given + const fieldMetadataItem = { + name: 'people', + type: FieldMetadataType.Relation, + }; + const relationObjectMetadataItem = mockedPersonObjectMetadataItem; + + // When + const { result } = renderHook( + () => + useFieldPreviewValue({ + fieldMetadataItem, + relationObjectMetadataItem, + }), + { wrapper: Wrapper }, + ); + + // Then + expect(result.current).toEqual({ + __typename: 'Person', + id: '', + name: { + firstName: 'John', + lastName: 'Doe', + }, + }); + }); + + it("returns the field's preview value for a Select field", () => { + // Given + const fieldName = 'stage'; + const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( + ({ name, type }) => + name === fieldName && type === FieldMetadataType.Select, + ); + + if (!fieldMetadataItem) { + throw new Error(`Field ${fieldName} not found`); + } + + // When + const { result } = renderHook( + () => useFieldPreviewValue({ fieldMetadataItem }), + { wrapper: Wrapper }, + ); + + // Then + expect(result.current).toBe('NEW'); + }); + + it("returns the field's preview value for a Multi-Select field", () => { + // Given + const options: FieldMetadataItemOption[] = [ + { + color: 'blue', + label: 'Blue', + value: 'BLUE', + id: '1', + position: 0, + }, + { + color: 'red', + label: 'Red', + value: 'RED', + id: '2', + position: 1, + }, + { + color: 'green', + label: 'Green', + value: 'GREEN', + id: '3', + position: 2, + }, + ]; + const fieldMetadataItem = { + name: 'industry', + type: FieldMetadataType.MultiSelect, + options, + }; + + // When + const { result } = renderHook( + () => useFieldPreviewValue({ fieldMetadataItem }), + { wrapper: Wrapper }, + ); + + // Then + expect(result.current).toEqual(options.map(({ value }) => value)); + }); + + it("returns the field's preview value for other field types", () => { + // Given + const fieldName = 'employees'; + const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + ({ name }) => name === fieldName, + ); + + if (!fieldMetadataItem) { + throw new Error(`Field ${fieldName} not found`); + } + + // When + const { result } = renderHook( + () => useFieldPreviewValue({ fieldMetadataItem }), + { wrapper: Wrapper }, + ); + + // Then + expect(result.current).toBe(2000); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreview.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreview.ts deleted file mode 100644 index 26ec7fb563..0000000000 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreview.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty'; -import { getFieldDefaultPreviewValue } from '@/settings/data-model/utils/getFieldDefaultPreviewValue'; -import { getFieldPreviewValueFromRecord } from '@/settings/data-model/utils/getFieldPreviewValueFromRecord'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -type UseFieldPreviewParams = { - fieldMetadataItem: Pick< - FieldMetadataItem, - 'icon' | 'type' | 'options' | 'defaultValue' - > & { - // id and name are undefined in create mode (field does not exist yet) - // and are defined in edit mode. - id?: string; - name?: string; - }; - objectMetadataItem: ObjectMetadataItem; - relationObjectMetadataItem?: ObjectMetadataItem; -}; - -export const useFieldPreview = ({ - fieldMetadataItem, - objectMetadataItem, - relationObjectMetadataItem, -}: UseFieldPreviewParams) => { - const isLabelIdentifier = - !!fieldMetadataItem.id && - !!fieldMetadataItem.name && - isLabelIdentifierField({ - fieldMetadataItem: { - id: fieldMetadataItem.id, - name: fieldMetadataItem.name, - }, - objectMetadataItem, - }); - - const { records } = useFindManyRecords({ - objectNameSingular: objectMetadataItem.nameSingular, - limit: 1, - skip: !fieldMetadataItem.name, - orderBy: { - [fieldMetadataItem.name ?? '']: 'AscNullsLast', - }, - }); - const [firstRecord] = records; - - const fieldPreviewValueFromFirstRecord = - firstRecord && fieldMetadataItem.name - ? getFieldPreviewValueFromRecord({ - record: firstRecord, - fieldMetadataItem: { - name: fieldMetadataItem.name, - type: fieldMetadataItem.type, - }, - }) - : null; - - const isValueFromFirstRecord = - firstRecord && - !isFieldValueEmpty({ - fieldDefinition: { type: fieldMetadataItem.type }, - fieldValue: fieldPreviewValueFromFirstRecord, - selectOptionValues: fieldMetadataItem.options?.map( - (option) => option.value, - ), - }); - - const { records: relationRecords } = useFindManyRecords({ - objectNameSingular: - relationObjectMetadataItem?.nameSingular || - CoreObjectNameSingular.Company, - limit: 1, - skip: - !relationObjectMetadataItem || - fieldMetadataItem.type !== FieldMetadataType.Relation || - isValueFromFirstRecord, - }); - const [firstRelationRecord] = relationRecords; - - const fieldPreviewValue = isValueFromFirstRecord - ? fieldPreviewValueFromFirstRecord - : firstRelationRecord ?? - getFieldDefaultPreviewValue({ - fieldMetadataItem, - objectMetadataItem, - relationObjectMetadataItem, - }); - - const fieldName = - fieldMetadataItem.name || `${fieldMetadataItem.type}-new-field`; - const entityId = isValueFromFirstRecord - ? firstRecord.id - : `${objectMetadataItem.nameSingular}-${fieldMetadataItem.name}-preview-field-form`; - - return { - entityId, - fieldName, - fieldPreviewValue, - isLabelIdentifier, - record: isValueFromFirstRecord ? firstRecord : null, - }; -}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts new file mode 100644 index 0000000000..422a7075f7 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts @@ -0,0 +1,51 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useRelationFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useRelationFieldPreviewValue'; +import { getCurrencyFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue'; +import { getFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getFieldPreviewValue'; +import { getMultiSelectFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue'; +import { getSelectFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +type UseFieldPreviewParams = { + fieldMetadataItem: Pick< + FieldMetadataItem, + 'type' | 'options' | 'defaultValue' + >; + relationObjectMetadataItem?: ObjectMetadataItem; + skip?: boolean; +}; + +export const useFieldPreviewValue = ({ + fieldMetadataItem, + relationObjectMetadataItem, + skip, +}: UseFieldPreviewParams) => { + const relationFieldPreviewValue = useRelationFieldPreviewValue({ + relationObjectMetadataItem: relationObjectMetadataItem ?? { + fields: [], + labelSingular: '', + nameSingular: CoreObjectNameSingular.Company, + }, + skip: + skip || + fieldMetadataItem.type !== FieldMetadataType.Relation || + !relationObjectMetadataItem, + }); + + if (skip === true) return null; + + switch (fieldMetadataItem.type) { + case FieldMetadataType.Currency: + return getCurrencyFieldPreviewValue({ fieldMetadataItem }); + case FieldMetadataType.Relation: + return relationFieldPreviewValue; + case FieldMetadataType.Select: + return getSelectFieldPreviewValue({ fieldMetadataItem }); + case FieldMetadataType.MultiSelect: + return getMultiSelectFieldPreviewValue({ fieldMetadataItem }); + default: + return getFieldPreviewValue({ fieldMetadataItem }); + } +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/usePreviewRecord.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/usePreviewRecord.ts new file mode 100644 index 0000000000..d201dd84cc --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/usePreviewRecord.ts @@ -0,0 +1,65 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { getFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getFieldPreviewValue'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { isDefined } from '~/utils/isDefined'; +import { pascalCase } from '~/utils/string/pascalCase'; + +type UsePreviewRecordParams = { + objectMetadataItem: Pick< + ObjectMetadataItem, + | 'fields' + | 'labelIdentifierFieldMetadataId' + | 'labelSingular' + | 'nameSingular' + >; + skip?: boolean; +}; + +export const usePreviewRecord = ({ + objectMetadataItem, + skip: skipFromProps, +}: UsePreviewRecordParams): ObjectRecord | null => { + const labelIdentifierFieldMetadataItem = + getLabelIdentifierFieldMetadataItem(objectMetadataItem); + const skip = skipFromProps || !labelIdentifierFieldMetadataItem; + + const { records } = useFindManyRecords({ + objectNameSingular: objectMetadataItem.nameSingular, + limit: 1, + skip, + }); + + if (skip) return null; + + const [firstRecord] = records; + + if ( + isDefined(firstRecord) && + !isFieldValueEmpty({ + fieldDefinition: { type: labelIdentifierFieldMetadataItem.type }, + fieldValue: firstRecord?.[labelIdentifierFieldMetadataItem.name], + }) + ) { + return firstRecord; + } + + const fieldPreviewValue = + labelIdentifierFieldMetadataItem.type === FieldMetadataType.Text + ? objectMetadataItem.labelSingular + : getFieldPreviewValue({ + fieldMetadataItem: labelIdentifierFieldMetadataItem, + }); + + const placeholderRecord = { + __typename: pascalCase(objectMetadataItem.nameSingular), + id: '', + [labelIdentifierFieldMetadataItem.name]: fieldPreviewValue, + }; + + // If no record was found, or if the label identifier field value is empty, display a placeholder record + return placeholderRecord; +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useRelationFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useRelationFieldPreviewValue.ts new file mode 100644 index 0000000000..77876f032f --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useRelationFieldPreviewValue.ts @@ -0,0 +1,22 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { usePreviewRecord } from '@/settings/data-model/fields/preview/hooks/usePreviewRecord'; + +type UseRelationFieldPreviewParams = { + relationObjectMetadataItem: Pick< + ObjectMetadataItem, + | 'fields' + | 'labelIdentifierFieldMetadataId' + | 'labelSingular' + | 'nameSingular' + >; + skip?: boolean; +}; + +export const useRelationFieldPreviewValue = ({ + relationObjectMetadataItem, + skip, +}: UseRelationFieldPreviewParams) => + usePreviewRecord({ + objectMetadataItem: relationObjectMetadataItem, + skip, + }); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getCurrencyFieldPreviewValue.test.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getCurrencyFieldPreviewValue.test.ts new file mode 100644 index 0000000000..a8d9b9a4af --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getCurrencyFieldPreviewValue.test.ts @@ -0,0 +1,126 @@ +import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { + mockedCompanyObjectMetadataItem, + mockedOpportunityObjectMetadataItem, +} from '~/testing/mock-data/metadata'; + +import { getCurrencyFieldPreviewValue } from '../getCurrencyFieldPreviewValue'; + +describe('getCurrencyFieldPreviewValue', () => { + it('returns null if the field is not a Currency field', () => { + // Given + const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + ({ type }) => type !== FieldMetadataType.Currency, + ); + + if (!fieldMetadataItem) { + throw new Error('Field not found'); + } + + // When + const previewValue = getCurrencyFieldPreviewValue({ fieldMetadataItem }); + + // Then + expect(previewValue).toBeNull(); + }); + + const fieldName = 'amount'; + const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( + ({ name, type }) => + name === fieldName && type === FieldMetadataType.Currency, + ); + + if (!fieldMetadataItem) { + throw new Error(`Field '${fieldName}' not found`); + } + + it("returns the parsed defaultValue if a valid defaultValue is found in the field's metadata", () => { + // Given + const defaultValue = { + amountMicros: 3000000000, + currencyCode: `'${CurrencyCode.EUR}'`, + }; + const fieldMetadataItemWithDefaultValue = { + ...fieldMetadataItem, + defaultValue, + }; + + // When + const previewValue = getCurrencyFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithDefaultValue, + }); + + // Then + expect(previewValue).toEqual({ + amountMicros: defaultValue.amountMicros, + currencyCode: CurrencyCode.EUR, + }); + }); + + it("returns a placeholder amountMicros if it is empty in the field's metadata defaultValue", () => { + // Given + const defaultValue = { + amountMicros: null, + currencyCode: `'${CurrencyCode.EUR}'`, + }; + const fieldMetadataItemWithDefaultValue = { + ...fieldMetadataItem, + defaultValue, + }; + + // When + const previewValue = getCurrencyFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithDefaultValue, + }); + + // Then + expect(previewValue).toEqual({ + amountMicros: 2000000000, + currencyCode: CurrencyCode.EUR, + }); + }); + + it("returns a placeholder default value if the defaultValue found in the field's metadata is invalid", () => { + // Given + const defaultValue = { + amountMicros: null, + currencyCode: "''", + }; + const fieldMetadataItemWithDefaultValue = { + ...fieldMetadataItem, + defaultValue, + }; + + // When + const previewValue = getCurrencyFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithDefaultValue, + }); + + // Then + expect(previewValue).toEqual({ + amountMicros: 2000000000, + currencyCode: CurrencyCode.USD, + }); + }); + + it("returns a placeholder default value if no defaultValue is found in the field's metadata", () => { + // Given + const defaultValue = null; + const fieldMetadataItemWithDefaultValue = { + ...fieldMetadataItem, + defaultValue, + }; + + // When + const previewValue = getCurrencyFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithDefaultValue, + }); + + // Then + expect(previewValue).toEqual({ + amountMicros: 2000000000, + currencyCode: CurrencyCode.USD, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts new file mode 100644 index 0000000000..69db5eb44a --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts @@ -0,0 +1,85 @@ +import { getFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getFieldPreviewValue'; +import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { + mockedCompanyObjectMetadataItem, + mockedCustomObjectMetadataItem, + mockedPersonObjectMetadataItem, +} from '~/testing/mock-data/metadata'; + +describe('getFieldPreviewValue', () => { + it("returns the field's defaultValue from metadata if it exists", () => { + // Given + const fieldName = 'idealCustomerProfile'; + const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + ({ name }) => name === fieldName, + ); + + if (!fieldMetadataItem) { + throw new Error(`Field '${fieldName}' not found`); + } + + // When + const result = getFieldPreviewValue({ fieldMetadataItem }); + + // Then + expect(result).toBe(false); + }); + + it('returns a placeholder defaultValue if the field metadata does not have a defaultValue', () => { + // Given + const fieldName = 'employees'; + const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + ({ name }) => name === fieldName, + ); + + if (!fieldMetadataItem) { + throw new Error(`Field '${fieldName}' not found`); + } + + // When + const result = getFieldPreviewValue({ fieldMetadataItem }); + + // Then + expect(result).toBe(2000); + expect(result).toBe( + getSettingsFieldTypeConfig(FieldMetadataType.Number)?.defaultValue, + ); + }); + + it('returns null if the field is supported in Settings but has no pre-configured placeholder defaultValue', () => { + // Given + const fieldName = 'company'; + const fieldMetadataItem = mockedPersonObjectMetadataItem.fields.find( + ({ name }) => name === fieldName, + ); + + if (!fieldMetadataItem) { + throw new Error(`Field '${fieldName}' not found`); + } + + // When + const result = getFieldPreviewValue({ fieldMetadataItem }); + + // Then + expect(result).toBeNull(); + }); + + it('returns null if the field is not supported in Settings', () => { + // Given + const fieldName = 'position'; + const fieldMetadataItem = mockedCustomObjectMetadataItem.fields.find( + ({ name }) => name === fieldName, + ); + + if (!fieldMetadataItem) { + throw new Error(`Field '${fieldName}' not found`); + } + + // When + const result = getFieldPreviewValue({ fieldMetadataItem }); + + // Then + expect(result).toBeNull(); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getMultiSelectFieldPreviewValue.test.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getMultiSelectFieldPreviewValue.test.ts new file mode 100644 index 0000000000..824d6e8201 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getMultiSelectFieldPreviewValue.test.ts @@ -0,0 +1,129 @@ +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { + mockedCompanyObjectMetadataItem, + mockedCustomObjectMetadataItem, +} from '~/testing/mock-data/metadata'; + +import { getMultiSelectFieldPreviewValue } from '../getMultiSelectFieldPreviewValue'; + +describe('getMultiSelectFieldPreviewValue', () => { + it('returns null if the field is not a Multi-Select field', () => { + // Given + const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + ({ type }) => type !== FieldMetadataType.MultiSelect, + ); + + if (!fieldMetadataItem) { + throw new Error('Field not found'); + } + + // When + const previewValue = getMultiSelectFieldPreviewValue({ fieldMetadataItem }); + + // Then + expect(previewValue).toBeNull(); + }); + + const fieldName = 'priority'; + const selectFieldMetadataItem = mockedCustomObjectMetadataItem.fields.find( + ({ name, type }) => name === fieldName && type === FieldMetadataType.Select, + ); + + if (!selectFieldMetadataItem) { + throw new Error(`Field '${fieldName}' not found`); + } + + const fieldMetadataItem = { + ...selectFieldMetadataItem, + type: FieldMetadataType.MultiSelect, + }; + + it("returns the defaultValue as an option value if a valid defaultValue is found in the field's metadata", () => { + // Given + const defaultValue = ["'MEDIUM'", "'LOW'"]; + const fieldMetadataItemWithDefaultValue = { + ...fieldMetadataItem, + defaultValue, + }; + + // When + const previewValue = getMultiSelectFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithDefaultValue, + }); + + // Then + expect(previewValue).toEqual(['MEDIUM', 'LOW']); + }); + + it("returns all option values if no defaultValue was found in the field's metadata", () => { + // Given + const defaultValue = null; + const fieldMetadataItemWithDefaultValue = { + ...fieldMetadataItem, + defaultValue, + }; + + // When + const previewValue = getMultiSelectFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithDefaultValue, + }); + + // Then + expect(previewValue).toEqual(['LOW', 'MEDIUM', 'HIGH']); + expect(previewValue).toEqual( + fieldMetadataItemWithDefaultValue.options?.map(({ value }) => value), + ); + }); + + it("returns the first option value if the defaultValue found in the field's metadata is invalid", () => { + // Given + const defaultValue = false; + const fieldMetadataItemWithDefaultValue = { + ...fieldMetadataItem, + defaultValue, + }; + + // When + const previewValue = getMultiSelectFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithDefaultValue, + }); + + // Then + expect(previewValue).toEqual(['LOW', 'MEDIUM', 'HIGH']); + expect(previewValue).toEqual( + fieldMetadataItemWithDefaultValue.options?.map(({ value }) => value), + ); + }); + + it('returns null if options are not defined', () => { + // Given + const fieldMetadataItemWithNoOptions = { + ...fieldMetadataItem, + options: undefined, + }; + + // When + const previewValue = getMultiSelectFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithNoOptions, + }); + + // Then + expect(previewValue).toBeNull(); + }); + + it('returns null if options array is empty', () => { + // Given + const fieldMetadataItemWithEmptyOptions = { + ...fieldMetadataItem, + options: [], + }; + + // When + const previewValue = getMultiSelectFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithEmptyOptions, + }); + + // Then + expect(previewValue).toBeNull(); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getSelectFieldPreviewValue.test.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getSelectFieldPreviewValue.test.ts new file mode 100644 index 0000000000..3572d58d90 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getSelectFieldPreviewValue.test.ts @@ -0,0 +1,124 @@ +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { + mockedCompanyObjectMetadataItem, + mockedCustomObjectMetadataItem, +} from '~/testing/mock-data/metadata'; + +import { getSelectFieldPreviewValue } from '../getSelectFieldPreviewValue'; + +describe('getSelectFieldPreviewValue', () => { + it('returns null if the field is not a Select field', () => { + // Given + const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + ({ type }) => type !== FieldMetadataType.Select, + ); + + if (!fieldMetadataItem) { + throw new Error('Field not found'); + } + + // When + const previewValue = getSelectFieldPreviewValue({ fieldMetadataItem }); + + // Then + expect(previewValue).toBeNull(); + }); + + const fieldName = 'priority'; + const fieldMetadataItem = mockedCustomObjectMetadataItem.fields.find( + ({ name, type }) => name === fieldName && type === FieldMetadataType.Select, + ); + + if (!fieldMetadataItem) { + throw new Error(`Field '${fieldName}' not found`); + } + + it("returns the defaultValue as an option value if a valid defaultValue is found in the field's metadata", () => { + // Given + const defaultValue = "'MEDIUM'"; + const fieldMetadataItemWithDefaultValue = { + ...fieldMetadataItem, + defaultValue, + }; + + // When + const previewValue = getSelectFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithDefaultValue, + }); + + // Then + expect(previewValue).toBe('MEDIUM'); + }); + + it("returns the first option value if no defaultValue was found in the field's metadata", () => { + // Given + const defaultValue = null; + const fieldMetadataItemWithDefaultValue = { + ...fieldMetadataItem, + defaultValue, + }; + + // When + const previewValue = getSelectFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithDefaultValue, + }); + + // Then + expect(previewValue).toBe('LOW'); + expect(previewValue).toBe( + fieldMetadataItemWithDefaultValue.options?.[0]?.value, + ); + }); + + it("returns the first option value if the defaultValue found in the field's metadata is invalid", () => { + // Given + const defaultValue = false; + const fieldMetadataItemWithDefaultValue = { + ...fieldMetadataItem, + defaultValue, + }; + + // When + const previewValue = getSelectFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithDefaultValue, + }); + + // Then + expect(previewValue).toBe('LOW'); + expect(previewValue).toBe( + fieldMetadataItemWithDefaultValue.options?.[0]?.value, + ); + }); + + it('returns null if options are not defined', () => { + // Given + const fieldMetadataItemWithNoOptions = { + ...fieldMetadataItem, + options: undefined, + }; + + // When + const previewValue = getSelectFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithNoOptions, + }); + + // Then + expect(previewValue).toBeNull(); + }); + + it('returns null if options array is empty', () => { + // Given + const fieldMetadataItemWithEmptyOptions = { + ...fieldMetadataItem, + options: [], + }; + + // When + const previewValue = getSelectFieldPreviewValue({ + fieldMetadataItem: fieldMetadataItemWithEmptyOptions, + }); + + // Then + expect(previewValue).toBeNull(); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue.ts new file mode 100644 index 0000000000..408f465058 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue.ts @@ -0,0 +1,32 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; +import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata'; +import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema'; +import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; + +export const getCurrencyFieldPreviewValue = ({ + fieldMetadataItem, +}: { + fieldMetadataItem: Pick< + FieldMetadataItem, + 'defaultValue' | 'options' | 'type' + >; +}): FieldCurrencyValue | null => { + if (fieldMetadataItem.type !== FieldMetadataType.Currency) return null; + + const placeholderDefaultValue = getSettingsFieldTypeConfig( + FieldMetadataType.Currency, + ).defaultValue; + + return currencyFieldDefaultValueSchema + .transform((value) => ({ + amountMicros: value.amountMicros || placeholderDefaultValue.amountMicros, + currencyCode: stripSimpleQuotesFromString( + value.currencyCode, + ) as CurrencyCode, + })) + .catch(placeholderDefaultValue) + .parse(fieldMetadataItem.defaultValue); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts new file mode 100644 index 0000000000..c207ee4fd9 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts @@ -0,0 +1,34 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty'; +import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; +import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings'; +import { isDefined } from '~/utils/isDefined'; + +export const getFieldPreviewValue = ({ + fieldMetadataItem, +}: { + fieldMetadataItem: Pick; +}) => { + if (!isFieldTypeSupportedInSettings(fieldMetadataItem.type)) return null; + + if ( + !isFieldValueEmpty({ + fieldDefinition: { type: fieldMetadataItem.type }, + fieldValue: fieldMetadataItem.defaultValue, + }) + ) { + return fieldMetadataItem.defaultValue; + } + + const fieldTypeConfig = getSettingsFieldTypeConfig(fieldMetadataItem.type); + + if ( + isDefined(fieldTypeConfig) && + 'defaultValue' in fieldTypeConfig && + isDefined(fieldTypeConfig.defaultValue) + ) { + return fieldTypeConfig.defaultValue; + } + + return null; +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue.ts new file mode 100644 index 0000000000..acc68ad9f1 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue.ts @@ -0,0 +1,36 @@ +import { isNonEmptyArray } from '@apollo/client/utilities'; +import { isNonEmptyString } from '@sniptt/guards'; + +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata'; +import { multiSelectFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/multiSelectFieldDefaultValueSchema'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { isDefined } from '~/utils/isDefined'; +import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; + +export const getMultiSelectFieldPreviewValue = ({ + fieldMetadataItem, +}: { + fieldMetadataItem: Pick< + FieldMetadataItem, + 'defaultValue' | 'options' | 'type' + >; +}): FieldMultiSelectValue => { + if ( + fieldMetadataItem.type !== FieldMetadataType.MultiSelect || + !fieldMetadataItem.options?.length + ) { + return null; + } + + const allOptionValues = fieldMetadataItem.options.map(({ value }) => value); + + return multiSelectFieldDefaultValueSchema(fieldMetadataItem.options) + .refine(isDefined) + .transform((value) => + value.map(stripSimpleQuotesFromString).filter(isNonEmptyString), + ) + .refine(isNonEmptyArray) + .catch(allOptionValues) + .parse(fieldMetadataItem.defaultValue); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue.ts new file mode 100644 index 0000000000..40e3f85d03 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue.ts @@ -0,0 +1,33 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { FieldSelectValue } from '@/object-record/record-field/types/FieldMetadata'; +import { selectFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/selectFieldDefaultValueSchema'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { isDefined } from '~/utils/isDefined'; +import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; + +export const getSelectFieldPreviewValue = ({ + fieldMetadataItem, +}: { + fieldMetadataItem: Pick< + FieldMetadataItem, + 'defaultValue' | 'options' | 'type' + >; +}): FieldSelectValue => { + if ( + fieldMetadataItem.type !== FieldMetadataType.Select || + !fieldMetadataItem.options?.length + ) { + return null; + } + + const firstOptionValue = fieldMetadataItem.options[0].value; + + return selectFieldDefaultValueSchema(fieldMetadataItem.options) + .refine(isDefined) + .transform(stripSimpleQuotesFromString) + .refine(isNonEmptyString) + .catch(firstOptionValue) + .parse(fieldMetadataItem.defaultValue); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/utils/__tests__/getFieldDefaultPreviewValue.test.ts b/packages/twenty-front/src/modules/settings/data-model/utils/__tests__/getFieldDefaultPreviewValue.test.ts deleted file mode 100644 index 3058c596ce..0000000000 --- a/packages/twenty-front/src/modules/settings/data-model/utils/__tests__/getFieldDefaultPreviewValue.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { - mockedCompanyObjectMetadataItem, - mockedOpportunityObjectMetadataItem, - mockedPersonObjectMetadataItem, -} from '~/testing/mock-data/metadata'; - -import { getFieldDefaultPreviewValue } from '../getFieldDefaultPreviewValue'; - -describe('getFieldDefaultPreviewValue', () => { - describe('SELECT field', () => { - it('returns the default select option value', () => { - // Given - const objectMetadataItem = mockedOpportunityObjectMetadataItem; - const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( - ({ name }) => name === 'stage', - )!; - - // When - const result = getFieldDefaultPreviewValue({ - objectMetadataItem, - fieldMetadataItem: { ...fieldMetadataItem, defaultValue: "'MEETING'" }, - }); - - // Then - expect(result).toEqual('MEETING'); - }); - - it('returns the first select option if no default option was found', () => { - // Given - const objectMetadataItem = mockedOpportunityObjectMetadataItem; - const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( - ({ name }) => name === 'stage', - )!; - - // When - const result = getFieldDefaultPreviewValue({ - objectMetadataItem, - fieldMetadataItem: { ...fieldMetadataItem, defaultValue: null }, - }); - - // Then - expect(result).toEqual(fieldMetadataItem.options![0].value); - }); - }); - - describe('RELATION field', () => { - it('returns a record with a default label identifier (if relation label identifier type !== TEXT)', () => { - // Given - const objectMetadataItem = mockedCompanyObjectMetadataItem; - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( - ({ name }) => name === 'people', - )!; - const relationObjectMetadataItem = mockedPersonObjectMetadataItem; - - // When - const result = getFieldDefaultPreviewValue({ - objectMetadataItem, - fieldMetadataItem, - relationObjectMetadataItem, - }); - - // Then - expect(result).toEqual({ - name: { - firstName: 'John', - lastName: 'Doe', - }, - }); - }); - - it('returns a record with the relation object label singular as label identifier (if relation label identifier type === TEXT)', () => { - // Given - const objectMetadataItem = mockedPersonObjectMetadataItem; - const fieldMetadataItem = mockedPersonObjectMetadataItem.fields.find( - ({ name }) => name === 'company', - )!; - const relationObjectMetadataItem = mockedCompanyObjectMetadataItem; - - // When - const result = getFieldDefaultPreviewValue({ - objectMetadataItem, - fieldMetadataItem, - relationObjectMetadataItem, - }); - - // Then - expect(result).toEqual({ - name: 'Company', - }); - }); - - it('returns null if the relation object does not have a label identifier field', () => { - // Given - const objectMetadataItem = mockedPersonObjectMetadataItem; - const fieldMetadataItem = mockedPersonObjectMetadataItem.fields.find( - ({ name }) => name === 'company', - )!; - const relationObjectMetadataItem: ObjectMetadataItem = { - ...mockedCompanyObjectMetadataItem, - labelIdentifierFieldMetadataId: null, - fields: mockedCompanyObjectMetadataItem.fields.filter( - ({ name }) => name !== 'name', - ), - }; - - // When - const result = getFieldDefaultPreviewValue({ - objectMetadataItem, - fieldMetadataItem, - relationObjectMetadataItem, - }); - - // Then - expect(result).toBeNull(); - }); - }); - - describe('Other fields', () => { - it('returns the object singular name as default value for the label identifier field (type TEXT)', () => { - // Given - const objectMetadataItem = mockedCompanyObjectMetadataItem; - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( - ({ name }) => name === 'name', - )!; - - // When - const result = getFieldDefaultPreviewValue({ - objectMetadataItem, - fieldMetadataItem, - }); - - // Then - expect(result).toBe('Company'); - }); - - it('returns a default value for the label identifier field (type FULL_NAME)', () => { - // Given - const objectMetadataItem = mockedPersonObjectMetadataItem; - const fieldMetadataItem = mockedPersonObjectMetadataItem.fields.find( - ({ name }) => name === 'name', - )!; - - // When - const result = getFieldDefaultPreviewValue({ - objectMetadataItem, - fieldMetadataItem, - }); - - // Then - expect(result).toEqual({ - firstName: 'John', - lastName: 'Doe', - }); - }); - - it('returns a default value for other field types', () => { - // Given - const objectMetadataItem = mockedCompanyObjectMetadataItem; - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( - ({ name }) => name === 'domainName', - )!; - - // When - const result = getFieldDefaultPreviewValue({ - objectMetadataItem, - fieldMetadataItem, - }); - - // Then - expect(result).toBe( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.', - ); - }); - }); -}); diff --git a/packages/twenty-front/src/modules/settings/data-model/utils/getFieldDefaultPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/utils/getFieldDefaultPreviewValue.ts deleted file mode 100644 index cd5efb967a..0000000000 --- a/packages/twenty-front/src/modules/settings/data-model/utils/getFieldDefaultPreviewValue.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { isString } from '@sniptt/guards'; - -import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; -import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; -import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { isDefined } from '~/utils/isDefined'; -import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; - -export const getFieldDefaultPreviewValue = ({ - fieldMetadataItem, - objectMetadataItem, - relationObjectMetadataItem, -}: { - fieldMetadataItem: Pick< - FieldMetadataItem, - 'type' | 'defaultValue' | 'options' - > & { - id?: string; - name?: string; - }; - objectMetadataItem: ObjectMetadataItem; - relationObjectMetadataItem?: ObjectMetadataItem; -}) => { - if (fieldMetadataItem.type === FieldMetadataType.Select) { - const defaultValue = isString(fieldMetadataItem.defaultValue) - ? stripSimpleQuotesFromString(fieldMetadataItem.defaultValue) - : null; - return defaultValue ?? fieldMetadataItem.options?.[0]?.value ?? null; - } - - if (fieldMetadataItem.type === FieldMetadataType.MultiSelect) { - const defaultValues = Array.isArray(fieldMetadataItem.defaultValue) - ? fieldMetadataItem.defaultValue?.map((defaultValue: `'${string}'`) => - stripSimpleQuotesFromString(defaultValue), - ) - : null; - return defaultValues?.length - ? defaultValues - : fieldMetadataItem.options?.map(({ value }) => value) ?? null; - } - - if ( - fieldMetadataItem.type === FieldMetadataType.Relation && - isDefined(relationObjectMetadataItem) - ) { - const relationLabelIdentifierFieldMetadataItem = - getLabelIdentifierFieldMetadataItem(relationObjectMetadataItem); - - if (!relationLabelIdentifierFieldMetadataItem) return null; - - const { type: relationLabelIdentifierFieldType } = - relationLabelIdentifierFieldMetadataItem; - const relationFieldTypeConfig = getSettingsFieldTypeConfig( - relationLabelIdentifierFieldType, - ); - - const defaultRelationLabelIdentifierFieldValue = - relationLabelIdentifierFieldType === FieldMetadataType.Text - ? relationObjectMetadataItem.labelSingular - : relationFieldTypeConfig?.defaultValue; - - const defaultRelationRecord = { - [relationLabelIdentifierFieldMetadataItem.name]: - defaultRelationLabelIdentifierFieldValue, - }; - - return defaultRelationRecord; - } - - const isLabelIdentifier = - !!fieldMetadataItem.id && - !!fieldMetadataItem.name && - isLabelIdentifierField({ - fieldMetadataItem: { - id: fieldMetadataItem.id, - name: fieldMetadataItem.name, - }, - objectMetadataItem, - }); - - const fieldTypeConfig = getSettingsFieldTypeConfig(fieldMetadataItem.type); - - return isLabelIdentifier && fieldMetadataItem.type === FieldMetadataType.Text - ? objectMetadataItem.labelSingular - : fieldTypeConfig?.defaultValue; -}; diff --git a/packages/twenty-front/src/modules/settings/data-model/utils/getSettingsFieldTypeConfig.ts b/packages/twenty-front/src/modules/settings/data-model/utils/getSettingsFieldTypeConfig.ts index 9d643ee6ad..3278a1dee4 100644 --- a/packages/twenty-front/src/modules/settings/data-model/utils/getSettingsFieldTypeConfig.ts +++ b/packages/twenty-front/src/modules/settings/data-model/utils/getSettingsFieldTypeConfig.ts @@ -1,8 +1,13 @@ import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; +import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -export const getSettingsFieldTypeConfig = (fieldType: FieldMetadataType) => - isFieldTypeSupportedInSettings(fieldType) +export const getSettingsFieldTypeConfig = ( + fieldType: T, +) => + (isFieldTypeSupportedInSettings(fieldType) ? SETTINGS_FIELD_TYPE_CONFIGS[fieldType] + : undefined) as T extends SettingsSupportedFieldType + ? (typeof SETTINGS_FIELD_TYPE_CONFIGS)[T] : undefined; diff --git a/packages/twenty-front/src/testing/mock-data/generated/standard-metadata-query-result.ts b/packages/twenty-front/src/testing/mock-data/generated/standard-metadata-query-result.ts index abedcd64b0..4446ad4f59 100644 --- a/packages/twenty-front/src/testing/mock-data/generated/standard-metadata-query-result.ts +++ b/packages/twenty-front/src/testing/mock-data/generated/standard-metadata-query-result.ts @@ -1,4 +1,4 @@ -import { id } from 'date-fns/locale'; +import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; import { FieldMetadataType, ObjectEdge, @@ -3787,10 +3787,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = isNullable: true, createdAt: '2024-04-08T12:48:49.538Z', updatedAt: '2024-04-08T12:48:49.538Z', - defaultValue: { - lastName: "''", - firstName: "''", - }, + defaultValue: null, relationDefinition: null, fromRelationMetadata: null, toRelationMetadata: null, @@ -3876,10 +3873,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = isNullable: true, createdAt: '2024-04-08T12:48:49.538Z', updatedAt: '2024-04-08T12:48:49.538Z', - defaultValue: { - url: "''", - label: "''", - }, + defaultValue: null, relationDefinition: null, fromRelationMetadata: null, toRelationMetadata: null, @@ -10562,7 +10556,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = updatedAt: '2024-04-08T12:48:49.538Z', defaultValue: { amountMicros: null, - currencyCode: "''", + currencyCode: `'${CurrencyCode.USD}'`, }, relationDefinition: null, fromRelationMetadata: null, @@ -10822,10 +10816,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = isNullable: true, createdAt: '2024-04-08T12:48:49.538Z', updatedAt: '2024-04-08T12:48:49.538Z', - defaultValue: { - url: "''", - label: "''", - }, + defaultValue: null, relationDefinition: null, fromRelationMetadata: null, toRelationMetadata: null, @@ -12259,7 +12250,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = nameSingular: 'company', namePlural: 'companies', isSystem: false, -isRemote: false, + isRemote: false, }, }, }, @@ -12345,7 +12336,7 @@ isRemote: false, nameSingular: 'opportunity', namePlural: 'opportunities', isSystem: false, -isRemote: false, + isRemote: false, }, }, }, @@ -12408,7 +12399,7 @@ isRemote: false, nameSingular: 'listing', namePlural: 'listings', isSystem: false, -isRemote: false, + isRemote: false, }, }, }, @@ -13027,7 +13018,7 @@ isRemote: false, nameSingular: 'opportunity', namePlural: 'opportunities', isSystem: false, -isRemote: false, + isRemote: false, }, }, }, @@ -13218,7 +13209,7 @@ isRemote: false, nameSingular: 'company', namePlural: 'companies', isSystem: false, -isRemote: false, + isRemote: false, }, }, relationDefinition: { diff --git a/packages/twenty-front/src/testing/mock-data/metadata.ts b/packages/twenty-front/src/testing/mock-data/metadata.ts index bfe6e2be96..94b5f910c2 100644 --- a/packages/twenty-front/src/testing/mock-data/metadata.ts +++ b/packages/twenty-front/src/testing/mock-data/metadata.ts @@ -1,6 +1,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems'; import { + FieldMetadataType, ObjectEdge, ObjectMetadataItemsQuery, } from '~/generated-metadata/graphql'; @@ -237,6 +238,48 @@ const customObjectMetadataItemEdge: ObjectEdge = { toRelationMetadata: null, }, }, + { + __typename: 'fieldEdge', + node: { + __typename: 'field', + id: 'e07fcc3f-beec-4d91-8488-9d1d2cfa5f99', + type: FieldMetadataType.Select, + name: 'priority', + label: 'Priority', + description: 'A custom Select example', + icon: 'IconWarning', + isCustom: true, + isActive: true, + isSystem: false, + options: [ + { + id: '2b98dc02-0d99-4f3e-890e-e2e6b8f3196c', + value: 'LOW', + label: 'Low', + color: 'turquoise', + }, + { + id: 'd925a8de-d8ec-4b59-a079-64f4012e3311', + value: 'MEDIUM', + label: 'Medium', + color: 'yellow', + }, + { + id: '3', + value: 'HIGH', + label: 'High', + color: 'red', + }, + ], + isNullable: true, + createdAt: '2024-04-08T12:48:49.538Z', + updatedAt: '2024-04-08T12:48:49.538Z', + defaultValue: null, + relationDefinition: null, + fromRelationMetadata: null, + toRelationMetadata: null, + }, + }, ], }, }, diff --git a/packages/twenty-front/src/utils/date-utils.ts b/packages/twenty-front/src/utils/date-utils.ts index e7678cf647..cb1a6e8c16 100644 --- a/packages/twenty-front/src/utils/date-utils.ts +++ b/packages/twenty-front/src/utils/date-utils.ts @@ -8,6 +8,8 @@ import { logError } from './logError'; export const DEFAULT_DATE_LOCALE = 'en-EN'; export const parseDate = (dateToParse: Date | string | number) => { + if (dateToParse === 'now') return DateTime.fromJSDate(new Date()); + let formattedDate: DateTime | null = null; if (!dateToParse) { diff --git a/yarn.lock b/yarn.lock index d28dfe0389..6efc5c2169 100644 --- a/yarn.lock +++ b/yarn.lock @@ -46640,7 +46640,7 @@ __metadata: vite-tsconfig-paths: "npm:^4.2.1" vitest: "npm:1.4.0" xlsx-ugnis: "npm:^0.19.3" - zod: "npm:^3.22.2" + zod: "npm:3.23.8" languageName: unknown linkType: soft @@ -49717,7 +49717,14 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.20.2, zod@npm:^3.22.2": +"zod@npm:3.23.8": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 8f14c87d6b1b53c944c25ce7a28616896319d95bc46a9660fe441adc0ed0a81253b02b5abdaeffedbeb23bdd25a0bf1c29d2c12dd919aef6447652dd295e3e69 + languageName: node + linkType: hard + +"zod@npm:^3.20.2": version: 3.22.4 resolution: "zod@npm:3.22.4" checksum: 7578ab283dac0eee66a0ad0fc4a7f28c43e6745aadb3a529f59a4b851aa10872b3890398b3160f257f4b6817b4ce643debdda4fb21a2c040adda7862cab0a587