feat: add feature to customize onClick behaviour for phone, email and links data type (#16265)
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 :** <img width="2084" height="1736" alt="image" src="https://github.com/user-attachments/assets/eb5d129c-e3e0-4334-b426-eb38d8a4840c" /> <img width="1474" height="1428" alt="image" src="https://github.com/user-attachments/assets/487f4e44-6151-4254-bc5c-5398be5fc087" /> <img width="1594" height="1398" alt="image" src="https://github.com/user-attachments/assets/abdbc213-7223-4d57-809e-4d55c90cda80" /> --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
This commit is contained in:
+20
-2
@@ -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 <EmailsDisplay value={fieldValue} />;
|
||||
const onClickAction = fieldDefinition.metadata.settings?.clickAction;
|
||||
|
||||
const handleEmailClick = (
|
||||
email: string,
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
) => {
|
||||
if (onClickAction === FieldMetadataSettingsOnClickAction.COPY) {
|
||||
event.preventDefault();
|
||||
copyToClipboard(email, t`Email copied to clipboard`);
|
||||
}
|
||||
};
|
||||
|
||||
return <EmailsDisplay value={fieldValue} onEmailClick={handleEmailClick} />;
|
||||
};
|
||||
|
||||
+21
-2
@@ -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 <LinksDisplay value={fieldValue} />;
|
||||
const onClickAction = fieldDefinition.metadata.settings?.clickAction;
|
||||
|
||||
const handleLinkClick = (
|
||||
url: string,
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
) => {
|
||||
if (onClickAction === FieldMetadataSettingsOnClickAction.COPY) {
|
||||
event.preventDefault();
|
||||
copyToClipboard(url, t`Link copied to clipboard`);
|
||||
}
|
||||
};
|
||||
|
||||
return <LinksDisplay value={fieldValue} onLinkClick={handleLinkClick} />;
|
||||
};
|
||||
|
||||
+8
-29
@@ -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<HTMLElement>,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(phoneNumber);
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Phone number copied to clipboard`,
|
||||
options: {
|
||||
icon: <IconCircleCheck size={16} color="green" />,
|
||||
duration: 2000,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error copying to clipboard`,
|
||||
options: {
|
||||
icon: <IconExclamationCircle size={16} color="red" />,
|
||||
duration: 2000,
|
||||
},
|
||||
});
|
||||
if (onClickAction === FieldMetadataSettingsOnClickAction.COPY) {
|
||||
event.preventDefault();
|
||||
copyToClipboard(phoneNumber, t`Phone number copied to clipboard`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+4
@@ -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: {},
|
||||
|
||||
+4
@@ -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: {},
|
||||
|
||||
+5
@@ -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<FieldEmailsValue | undefined>(
|
||||
|
||||
+5
@@ -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<FieldLinksValue | undefined>(
|
||||
|
||||
+5
@@ -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<FieldPhonesValue | undefined>(
|
||||
|
||||
Reference in New Issue
Block a user