From 8dbcd506ed7478218b9bcd8894f9eb5367d3fba8 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 12 Dec 2025 19:04:47 +0530 Subject: [PATCH] feat: add feature to customize onClick behaviour for phone, email and links data type (#16265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes Issue: #15797 This PR adds a configurable click behavior setting for Phone, Email, and Links field types, allowing users to customize what happens when clicking on these fields. A new option (**Click Behaviour**) to configure the default behaviour for onClick of data is added in the settings page (Settings → Data Model → Object → Field Edit) for Phone, Email and Links data types. Users can now choose between two actions: - **Copy to clipboard**: Copies the value to clipboard with a success toast message - **Open as link**: Opens the value as a link (tel:, mailto:, or http/https) The default behaviour is persisted for all these three types(phone- Copy to clipboard, email & links: open link) to maintain backward compatibility. **Screenshots :** image image image --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> --- .../display/components/EmailsFieldDisplay.tsx | 22 +++- .../display/components/LinksFieldDisplay.tsx | 23 +++- .../display/components/PhonesFieldDisplay.tsx | 37 ++----- .../perf/EmailsFieldDisplay.perf.stories.tsx | 4 + .../perf/LinksFieldDisplay.perf.stories.tsx | 4 + .../meta-types/hooks/useEmailsFieldDisplay.ts | 5 + .../meta-types/hooks/useLinksFieldDisplay.ts | 5 + .../meta-types/hooks/usePhonesFieldDisplay.ts | 5 + .../SettingsDataModelFieldIsUniqueForm.tsx | 2 - ...ettingsDataModelFieldOnClickActionForm.tsx | 100 ++++++++++++++++++ ...SettingsDataModelFieldSettingsFormCard.tsx | 37 ++++++- ...ingsDataModelFieldTextSettingsFormCard.tsx | 2 + ...ingsDataModelFieldDateSettingsFormCard.tsx | 2 + ...gsDataModelFieldNumberSettingsFormCard.tsx | 3 +- .../SettingsDataModelFieldPhonesForm.tsx | 11 +- ...gsDataModelFieldPhonesSettingsFormCard.tsx | 9 +- ...ModelFieldOnClickActionDescription.util.ts | 21 ++++ .../forms/utils/mergeSettingsSchema.util.ts | 12 +++ ...ttingsDataModelFieldOnClickActionSchema.ts | 11 ++ ...ngsDataModelFieldPreviewWidget.stories.tsx | 2 + .../display/components/EmailsDisplay.tsx | 23 +++- .../field/display/components/LinksDisplay.tsx | 20 +++- .../display/components/PhonesDisplay.tsx | 15 ++- .../data-model/SettingsObjectFieldEdit.tsx | 1 + .../types/FieldMetadataMultiItemSettings.ts | 6 ++ packages/twenty-shared/src/types/index.ts | 1 + 26 files changed, 323 insertions(+), 60 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldOnClickActionForm.tsx create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/forms/utils/getSettingsDataModelFieldOnClickActionDescription.util.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/forms/utils/mergeSettingsSchema.util.ts create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/forms/utils/settingsDataModelFieldOnClickActionSchema.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/EmailsFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/EmailsFieldDisplay.tsx index d0baeb1f62..72c18bfe77 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/EmailsFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/EmailsFieldDisplay.tsx @@ -1,8 +1,26 @@ import { useEmailsFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useEmailsFieldDisplay'; import { EmailsDisplay } from '@/ui/field/display/components/EmailsDisplay'; +import { useLingui } from '@lingui/react/macro'; +import React from 'react'; +import { FieldMetadataSettingsOnClickAction } from 'twenty-shared/types'; +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; export const EmailsFieldDisplay = () => { - const { fieldValue } = useEmailsFieldDisplay(); + const { fieldValue, fieldDefinition } = useEmailsFieldDisplay(); + const { copyToClipboard } = useCopyToClipboard(); + const { t } = useLingui(); - return ; + const onClickAction = fieldDefinition.metadata.settings?.clickAction; + + const handleEmailClick = ( + email: string, + event: React.MouseEvent, + ) => { + if (onClickAction === FieldMetadataSettingsOnClickAction.COPY) { + event.preventDefault(); + copyToClipboard(email, t`Email copied to clipboard`); + } + }; + + return ; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/LinksFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/LinksFieldDisplay.tsx index e54d72d6e5..2f82e9b5d1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/LinksFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/LinksFieldDisplay.tsx @@ -1,8 +1,27 @@ import { useLinksFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useLinksFieldDisplay'; import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay'; +import { useLingui } from '@lingui/react/macro'; +import React from 'react'; +import { FieldMetadataSettingsOnClickAction } from 'twenty-shared/types'; + +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; export const LinksFieldDisplay = () => { - const { fieldValue } = useLinksFieldDisplay(); + const { fieldValue, fieldDefinition } = useLinksFieldDisplay(); + const { copyToClipboard } = useCopyToClipboard(); + const { t } = useLingui(); - return ; + const onClickAction = fieldDefinition.metadata.settings?.clickAction; + + const handleLinkClick = ( + url: string, + event: React.MouseEvent, + ) => { + if (onClickAction === FieldMetadataSettingsOnClickAction.COPY) { + event.preventDefault(); + copyToClipboard(url, t`Link copied to clipboard`); + } + }; + + return ; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/PhonesFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/PhonesFieldDisplay.tsx index 631a025b8e..fca9ced8bb 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/PhonesFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/PhonesFieldDisplay.tsx @@ -1,48 +1,27 @@ import { useFieldFocus } from '@/object-record/record-field/ui/hooks/useFieldFocus'; import { usePhonesFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/usePhonesFieldDisplay'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { PhonesDisplay } from '@/ui/field/display/components/PhonesDisplay'; import { useLingui } from '@lingui/react/macro'; import React from 'react'; -import { useIcons } from 'twenty-ui/display'; +import { FieldMetadataSettingsOnClickAction } from 'twenty-shared/types'; +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; export const PhonesFieldDisplay = () => { - const { fieldValue } = usePhonesFieldDisplay(); - + const { fieldValue, fieldDefinition } = usePhonesFieldDisplay(); + const { copyToClipboard } = useCopyToClipboard(); const { isFocused } = useFieldFocus(); - const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); - - const { getIcon } = useIcons(); - const { t } = useLingui(); - const IconCircleCheck = getIcon('IconCircleCheck'); - const IconExclamationCircle = getIcon('IconExclamationCircle'); + const onClickAction = fieldDefinition.metadata.settings?.clickAction; const handleClick = async ( phoneNumber: string, event: React.MouseEvent, ) => { - event.preventDefault(); - - try { - await navigator.clipboard.writeText(phoneNumber); - enqueueSuccessSnackBar({ - message: t`Phone number copied to clipboard`, - options: { - icon: , - duration: 2000, - }, - }); - } catch { - enqueueErrorSnackBar({ - message: t`Error copying to clipboard`, - options: { - icon: , - duration: 2000, - }, - }); + if (onClickAction === FieldMetadataSettingsOnClickAction.COPY) { + event.preventDefault(); + copyToClipboard(phoneNumber, t`Phone number copied to clipboard`); } }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/EmailsFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/EmailsFieldDisplay.perf.stories.tsx index cd255aa4cf..b06f29e5ce 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/EmailsFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/EmailsFieldDisplay.perf.stories.tsx @@ -2,19 +2,23 @@ import { type Meta, type StoryObj } from '@storybook/react'; import { EmailsFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/EmailsFieldDisplay'; import { ComponentDecorator } from 'twenty-ui/testing'; +import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator'; import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; const meta: Meta = { title: 'UI/Data/Field/Display/EmailsFieldDisplay', decorators: [ + I18nFrontDecorator, MemoryRouterDecorator, getFieldDecorator('person', 'emails', { primaryEmail: 'test@test.com', additionalEmails: ['toto@test.com'], }), ComponentDecorator, + SnackBarDecorator, ], component: EmailsFieldDisplay, args: {}, diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/LinksFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/LinksFieldDisplay.perf.stories.tsx index 0fe52593f8..61529b297a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/LinksFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/LinksFieldDisplay.perf.stories.tsx @@ -5,7 +5,9 @@ import { FieldFocusContext } from '@/object-record/record-field/ui/contexts/Fiel import { FieldFocusContextProvider } from '@/object-record/record-field/ui/contexts/FieldFocusContextProvider'; import { LinksFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/LinksFieldDisplay'; import { ComponentDecorator } from 'twenty-ui/testing'; +import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator'; import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; @@ -22,6 +24,7 @@ const FieldFocusEffect = () => { const meta: Meta = { title: 'UI/Data/Field/Display/LinksFieldDisplay', decorators: [ + I18nFrontDecorator, MemoryRouterDecorator, getFieldDecorator('company', 'domainName', { primaryLinkUrl: 'https://www.google.com', @@ -29,6 +32,7 @@ const meta: Meta = { secondaryLinks: ['https://www.toto.com'], }), ComponentDecorator, + SnackBarDecorator, ], component: LinksFieldDisplay, args: {}, diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useEmailsFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useEmailsFieldDisplay.ts index f675c29b0b..8a49b65e87 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useEmailsFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useEmailsFieldDisplay.ts @@ -1,12 +1,17 @@ import { useContext } from 'react'; import { type FieldEmailsValue } from '@/object-record/record-field/ui/types/FieldMetadata'; +import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata'; +import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFieldEmails'; import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue'; +import { FieldMetadataType } from 'twenty-shared/types'; import { FieldContext } from '../../contexts/FieldContext'; export const useEmailsFieldDisplay = () => { const { recordId, fieldDefinition } = useContext(FieldContext); + assertFieldMetadata(FieldMetadataType.EMAILS, isFieldEmails, fieldDefinition); + const fieldName = fieldDefinition.metadata.fieldName; const fieldValue = useRecordFieldValue( diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useLinksFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useLinksFieldDisplay.ts index e6729754ac..c780e4135c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useLinksFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useLinksFieldDisplay.ts @@ -2,12 +2,17 @@ import { useContext } from 'react'; import { type FieldLinksValue } from '@/object-record/record-field/ui/types/FieldMetadata'; +import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata'; +import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks'; import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue'; +import { FieldMetadataType } from 'twenty-shared/types'; import { FieldContext } from '../../contexts/FieldContext'; export const useLinksFieldDisplay = () => { const { recordId, fieldDefinition } = useContext(FieldContext); + assertFieldMetadata(FieldMetadataType.LINKS, isFieldLinks, fieldDefinition); + const fieldName = fieldDefinition.metadata.fieldName; const fieldValue = useRecordFieldValue( diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/usePhonesFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/usePhonesFieldDisplay.ts index 948525fdfc..fea10a6d7f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/usePhonesFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/usePhonesFieldDisplay.ts @@ -2,12 +2,17 @@ import { useContext } from 'react'; import { type FieldPhonesValue } from '@/object-record/record-field/ui/types/FieldMetadata'; +import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata'; +import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones'; import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue'; +import { FieldMetadataType } from 'twenty-shared/types'; import { FieldContext } from '../../contexts/FieldContext'; export const usePhonesFieldDisplay = () => { const { recordId, fieldDefinition } = useContext(FieldContext); + assertFieldMetadata(FieldMetadataType.PHONES, isFieldPhones, fieldDefinition); + const fieldName = fieldDefinition.metadata.fieldName; const fieldValue = useRecordFieldValue( diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm.tsx index b16f01ab83..38971064e8 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm.tsx @@ -2,7 +2,6 @@ import { Controller, useFormContext } from 'react-hook-form'; import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { Separator } from '@/settings/components/Separator'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; import { canBeUnique } from '@/settings/data-model/fields/forms/utils/canBeUnique.util'; import { t } from '@lingui/core/macro'; @@ -66,7 +65,6 @@ export const SettingsDataModelFieldIsUniqueForm = ({ return ( <> - { + const { t } = useLingui(); + const { control } = + useFormContext(); + + const { fieldMetadataItem } = useFieldMetadataItemById( + existingFieldMetadataId, + ); + + const options = [ + { + label: t`Open as link`, + value: FieldMetadataSettingsOnClickAction.OPEN_LINK, + }, + { + label: t`Copy to clipboard`, + value: FieldMetadataSettingsOnClickAction.COPY, + }, + ]; + + const description = + getSettingsDataModelFieldOnClickActionDescription(fieldType); + + const existingSettings = + (fieldMetadataItem?.settings as FieldMetadataMultiItemSettings) ?? {}; + + return ( + { + const currentSettings = + (value as FieldMetadataMultiItemSettings | undefined) ?? {}; + + const clickAction = + currentSettings.clickAction ?? + FieldMetadataSettingsOnClickAction.OPEN_LINK; + + return ( + + + dropdownWidth={180} + value={clickAction} + onChange={(newValue) => + onChange({ + ...currentSettings, + clickAction: newValue, + }) + } + disabled={disabled} + dropdownId="field-click-behavior-select" + options={options} + selectSizeVariant="small" + withSearchInput={false} + /> + + ); + }} + /> + ); +}; 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 fb56567b91..ba0439d6a0 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 @@ -25,11 +25,14 @@ import { settingsDataModelFieldSelectFormSchema, } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm'; import { SettingsDataModelFieldSelectSettingsFormCard } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectSettingsFormCard'; -import { settingsDataModelFieldMaxValuesSchema } from '@/settings/data-model/fields/forms/utils/settingsDataModelFieldMaxValuesSchema'; import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget'; import { Separator } from '@/settings/components/Separator'; +import { SettingsDataModelFieldOnClickActionForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldOnClickActionForm'; import { SettingsDataModelFieldRelationFormCard } from '@/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationFormCard'; +import { mergeSettingsSchemas } from '@/settings/data-model/fields/forms/utils/mergeSettingsSchema.util'; +import { settingsDataModelFieldMaxValuesSchema } from '@/settings/data-model/fields/forms/utils/settingsDataModelFieldMaxValuesSchema'; +import { settingsDataModelFieldOnClickActionSchema } from '@/settings/data-model/fields/forms/utils/settingsDataModelFieldOnClickActionSchema'; import { useFormContext } from 'react-hook-form'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; @@ -91,17 +94,27 @@ const phonesFieldFormSchema = z const emailsFieldFormSchema = z .object({ type: z.literal(FieldMetadataType.EMAILS) }) - .extend(settingsDataModelFieldMaxValuesSchema.shape) + .merge( + mergeSettingsSchemas( + settingsDataModelFieldMaxValuesSchema, + settingsDataModelFieldOnClickActionSchema, + ), + ) .extend(isUniqueFieldFormSchema.shape); const linksFieldFormSchema = z .object({ type: z.literal(FieldMetadataType.LINKS) }) - .extend(settingsDataModelFieldMaxValuesSchema.shape) + .merge( + mergeSettingsSchemas( + settingsDataModelFieldMaxValuesSchema, + settingsDataModelFieldOnClickActionSchema, + ), + ) .extend(isUniqueFieldFormSchema.shape); const arrayFieldFormSchema = z .object({ type: z.literal(FieldMetadataType.ARRAY) }) - .extend(settingsDataModelFieldMaxValuesSchema.shape) + .merge(mergeSettingsSchemas(settingsDataModelFieldMaxValuesSchema)) .extend(isUniqueFieldFormSchema.shape); const otherFieldsFormSchema = z @@ -322,6 +335,22 @@ export const SettingsDataModelFieldSettingsFormCard = ({ )} + {[FieldMetadataType.EMAILS, FieldMetadataType.LINKS].includes( + fieldType, + ) && ( + <> + + + + )} + + - + + + { + switch (fieldType) { + case FieldMetadataType.PHONES: + return t`Choose what happens when you click a phone number`; + case FieldMetadataType.EMAILS: + return t`Choose what happens when you click an email`; + case FieldMetadataType.LINKS: + return t`Choose what happens when you click a link`; + default: + assertUnreachable(fieldType, `Invalid field type: ${fieldType}`); + } +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/utils/mergeSettingsSchema.util.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/utils/mergeSettingsSchema.util.ts new file mode 100644 index 0000000000..72a82c9cc2 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/utils/mergeSettingsSchema.util.ts @@ -0,0 +1,12 @@ +import z from 'zod'; + +export const mergeSettingsSchemas = ( + ...schemas: z.ZodObject<{ settings: z.ZodObject }>[] +) => { + return z.object({ + settings: schemas.reduce( + (acc, schema) => acc.merge(schema.shape.settings), + z.object({}) as z.ZodObject, + ), + }); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/utils/settingsDataModelFieldOnClickActionSchema.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/utils/settingsDataModelFieldOnClickActionSchema.ts new file mode 100644 index 0000000000..80baae0e07 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/utils/settingsDataModelFieldOnClickActionSchema.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +export const settingsDataModelFieldOnClickActionSchema = z.object({ + settings: z.object({ + clickAction: z.enum(['COPY', 'OPEN_LINK']).optional(), + }), +}); + +export type SettingsDataModelFieldOnClickActionFormValues = z.infer< + typeof settingsDataModelFieldOnClickActionSchema +>; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewWidget.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewWidget.stories.tsx index fbf50133fe..16400bb1a8 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewWidget.stories.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewWidget.stories.tsx @@ -4,6 +4,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; import { graphqlMocks } from '~/testing/graphqlMocks'; import { ComponentDecorator } from 'twenty-ui/testing'; +import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; @@ -25,6 +26,7 @@ const meta: Meta = { 'Modules/Settings/DataModel/Fields/Preview/SettingsDataModelFieldPreviewWidget', component: SettingsDataModelFieldPreviewWidget, decorators: [ + I18nFrontDecorator, MemoryRouterDecorator, ComponentDecorator, ObjectMetadataItemsDecorator, diff --git a/packages/twenty-front/src/modules/ui/field/display/components/EmailsDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/EmailsDisplay.tsx index db8d2a5ab4..a7616997fb 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/EmailsDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/EmailsDisplay.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import React, { useMemo } from 'react'; import { type FieldEmailsValue } from '@/object-record/record-field/ui/types/FieldMetadata'; import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList'; @@ -10,6 +10,7 @@ import { THEME_COMMON } from 'twenty-ui/theme'; type EmailsDisplayProps = { value?: FieldEmailsValue; isFocused?: boolean; + onEmailClick?: (email: string, event: React.MouseEvent) => void; }; const themeSpacing = THEME_COMMON.spacingMultiplicator; @@ -27,7 +28,11 @@ const StyledContainer = styled.div` width: 100%; `; -export const EmailsDisplay = ({ value, isFocused }: EmailsDisplayProps) => { +export const EmailsDisplay = ({ + value, + isFocused, + onEmailClick, +}: EmailsDisplayProps) => { const emails = useMemo( () => [ @@ -40,13 +45,23 @@ export const EmailsDisplay = ({ value, isFocused }: EmailsDisplayProps) => { return isFocused ? ( {emails.map((email, index) => ( - + onEmailClick?.(email, event)} + /> ))} ) : ( {emails.map((email, index) => ( - + onEmailClick?.(email, event)} + /> ))} ); diff --git a/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx index 265755a6c2..05e0f197dc 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import React, { useMemo } from 'react'; import { getFieldLinkDefinedLinks } from '@/object-record/record-field/ui/meta-types/input/utils/getFieldLinkDefinedLinks'; import { type FieldLinksValue } from '@/object-record/record-field/ui/types/FieldMetadata'; @@ -13,9 +13,10 @@ import { checkUrlType } from '~/utils/checkUrlType'; type LinksDisplayProps = { value?: FieldLinksValue; + onLinkClick?: (url: string, event: React.MouseEvent) => void; }; -export const LinksDisplay = ({ value }: LinksDisplayProps) => { +export const LinksDisplay = ({ value, onLinkClick }: LinksDisplayProps) => { const links = useMemo(() => { if (!isDefined(value)) { return []; @@ -43,9 +44,20 @@ export const LinksDisplay = ({ value }: LinksDisplayProps) => { {links.map(({ url, label, type }, index) => type === LinkType.LinkedIn || type === LinkType.Twitter ? ( - + onLinkClick?.(url, event)} + /> ) : ( - + onLinkClick?.(url, event)} + /> ), )} diff --git a/packages/twenty-front/src/modules/ui/field/display/components/PhonesDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/PhonesDisplay.tsx index 8bd17f7da2..8673c655dc 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/PhonesDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/PhonesDisplay.tsx @@ -75,13 +75,6 @@ export const PhonesDisplay = ({ } }; - const handleClick = ( - number: string, - event: React.MouseEvent, - ) => { - onPhoneNumberClick?.(number, event); - }; - return isFocused ? ( {phones.map(({ number, callingCode }, index) => { @@ -95,7 +88,9 @@ export const PhonesDisplay = ({ label={ parsedPhone ? parsedPhone.formatInternational() : invalidPhone } - onClick={(event) => handleClick(callingCode + number, event)} + onClick={(event) => + onPhoneNumberClick?.(callingCode + number, event) + } /> ); })} @@ -113,7 +108,9 @@ export const PhonesDisplay = ({ label={ parsedPhone ? parsedPhone.formatInternational() : invalidPhone } - onClick={(event) => handleClick(callingCode + number, event)} + onClick={(event) => + onPhoneNumberClick?.(callingCode + number, event) + } /> ); })} diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx index 6688041a2a..cd17d57f28 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -119,6 +119,7 @@ export const SettingsObjectFieldEdit = () => { label: fieldMetadataItem?.label ?? '', description: fieldMetadataItem?.description, isLabelSyncedWithName: fieldMetadataItem?.isLabelSyncedWithName ?? true, + settings: fieldMetadataItem?.settings, }, }); diff --git a/packages/twenty-shared/src/types/FieldMetadataMultiItemSettings.ts b/packages/twenty-shared/src/types/FieldMetadataMultiItemSettings.ts index 9c84e8379f..4ccf774141 100644 --- a/packages/twenty-shared/src/types/FieldMetadataMultiItemSettings.ts +++ b/packages/twenty-shared/src/types/FieldMetadataMultiItemSettings.ts @@ -1,3 +1,9 @@ +export enum FieldMetadataSettingsOnClickAction { + COPY = 'COPY', + OPEN_LINK = 'OPEN_LINK', +} + export type FieldMetadataMultiItemSettings = { maxNumberOfValues?: number; + clickAction?: FieldMetadataSettingsOnClickAction; }; diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index 38d036572d..3eedd2b2d4 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -84,6 +84,7 @@ export { FieldMetadataDefaultArray, } from './FieldMetadataDefaultValue'; export type { FieldMetadataMultiItemSettings } from './FieldMetadataMultiItemSettings'; +export { FieldMetadataSettingsOnClickAction } from './FieldMetadataMultiItemSettings'; export type { TagColor, FieldMetadataOptions } from './FieldMetadataOptions'; export { FieldMetadataDefaultOption,