Skip to content

Commit bc8d4fd

Browse files
committed
feat: add feature to customize on click behaviour for phone, email and links data type
Issue: #15797
1 parent 1038efa commit bc8d4fd

File tree

17 files changed

+328
-34
lines changed

17 files changed

+328
-34
lines changed
Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,37 @@
11
import { useEmailsFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useEmailsFieldDisplay';
22
import { EmailsDisplay } from '@/ui/field/display/components/EmailsDisplay';
3+
import { useLingui } from '@lingui/react/macro';
4+
import React from 'react';
5+
import {
6+
FieldClickAction,
7+
type FieldMetadataMultiItemSettings,
8+
} from 'twenty-shared/types';
9+
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
310

411
export const EmailsFieldDisplay = () => {
5-
const { fieldValue } = useEmailsFieldDisplay();
12+
const { fieldValue, fieldDefinition } = useEmailsFieldDisplay();
13+
const { copyToClipboard } = useCopyToClipboard();
14+
const { t } = useLingui();
615

7-
return <EmailsDisplay value={fieldValue} />;
16+
const settings = fieldDefinition.metadata
17+
.settings as FieldMetadataMultiItemSettings | null;
18+
const clickAction = settings?.clickAction ?? FieldClickAction.OPEN_LINK;
19+
20+
const handleEmailClick = (
21+
email: string,
22+
event: React.MouseEvent<HTMLElement>,
23+
) => {
24+
event.preventDefault();
25+
copyToClipboard(email, t`Email copied to clipboard`);
26+
};
27+
28+
return (
29+
<EmailsDisplay
30+
value={fieldValue}
31+
onEmailClick={
32+
clickAction === FieldClickAction.COPY ? handleEmailClick : undefined
33+
}
34+
clickAction={clickAction}
35+
/>
36+
);
837
};
Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,37 @@
11
import { useLinksFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useLinksFieldDisplay';
22
import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay';
3+
import { useLingui } from '@lingui/react/macro';
4+
import React from 'react';
5+
import {
6+
FieldClickAction,
7+
type FieldMetadataMultiItemSettings,
8+
} from 'twenty-shared/types';
9+
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
310

411
export const LinksFieldDisplay = () => {
5-
const { fieldValue } = useLinksFieldDisplay();
12+
const { fieldValue, fieldDefinition } = useLinksFieldDisplay();
13+
const { copyToClipboard } = useCopyToClipboard();
14+
const { t } = useLingui();
615

7-
return <LinksDisplay value={fieldValue} />;
16+
const settings = fieldDefinition.metadata
17+
.settings as FieldMetadataMultiItemSettings | null;
18+
const clickAction = settings?.clickAction ?? FieldClickAction.OPEN_LINK;
19+
20+
const handleLinkClick = (
21+
url: string,
22+
event: React.MouseEvent<HTMLElement>,
23+
) => {
24+
event.preventDefault();
25+
copyToClipboard(url, t`Link copied to clipboard`);
26+
};
27+
28+
return (
29+
<LinksDisplay
30+
value={fieldValue}
31+
onLinkClick={
32+
clickAction === FieldClickAction.COPY ? handleLinkClick : undefined
33+
}
34+
clickAction={clickAction}
35+
/>
36+
);
837
};

packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/PhonesFieldDisplay.tsx

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
44
import { PhonesDisplay } from '@/ui/field/display/components/PhonesDisplay';
55
import { useLingui } from '@lingui/react/macro';
66
import React from 'react';
7+
import {
8+
FieldClickAction,
9+
type FieldMetadataMultiItemSettings,
10+
} from 'twenty-shared/types';
711
import { useIcons } from 'twenty-ui/display';
812

913
export const PhonesFieldDisplay = () => {
10-
const { fieldValue } = usePhonesFieldDisplay();
14+
const { fieldValue, fieldDefinition } = usePhonesFieldDisplay();
1115

1216
const { isFocused } = useFieldFocus();
1317

@@ -20,29 +24,35 @@ export const PhonesFieldDisplay = () => {
2024
const IconCircleCheck = getIcon('IconCircleCheck');
2125
const IconExclamationCircle = getIcon('IconExclamationCircle');
2226

27+
const settings = fieldDefinition.metadata
28+
.settings as FieldMetadataMultiItemSettings | null;
29+
const clickAction = settings?.clickAction ?? FieldClickAction.COPY;
30+
2331
const handleClick = async (
2432
phoneNumber: string,
2533
event: React.MouseEvent<HTMLElement>,
2634
) => {
27-
event.preventDefault();
28-
29-
try {
30-
await navigator.clipboard.writeText(phoneNumber);
31-
enqueueSuccessSnackBar({
32-
message: t`Phone number copied to clipboard`,
33-
options: {
34-
icon: <IconCircleCheck size={16} color="green" />,
35-
duration: 2000,
36-
},
37-
});
38-
} catch {
39-
enqueueErrorSnackBar({
40-
message: t`Error copying to clipboard`,
41-
options: {
42-
icon: <IconExclamationCircle size={16} color="red" />,
43-
duration: 2000,
44-
},
45-
});
35+
if (clickAction === FieldClickAction.COPY) {
36+
event.preventDefault();
37+
38+
try {
39+
await navigator.clipboard.writeText(phoneNumber);
40+
enqueueSuccessSnackBar({
41+
message: t`Phone number copied to clipboard`,
42+
options: {
43+
icon: <IconCircleCheck size={16} color="green" />,
44+
duration: 2000,
45+
},
46+
});
47+
} catch {
48+
enqueueErrorSnackBar({
49+
message: t`Error copying to clipboard`,
50+
options: {
51+
icon: <IconExclamationCircle size={16} color="red" />,
52+
duration: 2000,
53+
},
54+
});
55+
}
4656
}
4757
};
4858

@@ -51,6 +61,7 @@ export const PhonesFieldDisplay = () => {
5161
value={fieldValue}
5262
isFocused={isFocused}
5363
onPhoneNumberClick={handleClick}
64+
clickAction={clickAction}
5465
/>
5566
);
5667
};

packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/EmailsFieldDisplay.perf.stories.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@ import { type Meta, type StoryObj } from '@storybook/react';
22

33
import { EmailsFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/EmailsFieldDisplay';
44
import { ComponentDecorator } from 'twenty-ui/testing';
5+
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
56
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
7+
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
68
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
79
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
810

911
const meta: Meta = {
1012
title: 'UI/Data/Field/Display/EmailsFieldDisplay',
1113
decorators: [
14+
I18nFrontDecorator,
1215
MemoryRouterDecorator,
1316
getFieldDecorator('person', 'emails', {
1417
primaryEmail: '[email protected]',
1518
additionalEmails: ['[email protected]'],
1619
}),
1720
ComponentDecorator,
21+
SnackBarDecorator,
1822
],
1923
component: EmailsFieldDisplay,
2024
args: {},

packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/LinksFieldDisplay.perf.stories.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { FieldFocusContext } from '@/object-record/record-field/ui/contexts/Fiel
55
import { FieldFocusContextProvider } from '@/object-record/record-field/ui/contexts/FieldFocusContextProvider';
66
import { LinksFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/LinksFieldDisplay';
77
import { ComponentDecorator } from 'twenty-ui/testing';
8+
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
89
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
10+
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
911
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
1012
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
1113

@@ -22,13 +24,15 @@ const FieldFocusEffect = () => {
2224
const meta: Meta = {
2325
title: 'UI/Data/Field/Display/LinksFieldDisplay',
2426
decorators: [
27+
I18nFrontDecorator,
2528
MemoryRouterDecorator,
2629
getFieldDecorator('company', 'domainName', {
2730
primaryLinkUrl: 'https://www.google.com',
2831
primaryLinkLabel: 'Google',
2932
secondaryLinks: ['https://www.toto.com'],
3033
}),
3134
ComponentDecorator,
35+
SnackBarDecorator,
3236
],
3337
component: LinksFieldDisplay,
3438
args: {},
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Controller, useFormContext } from 'react-hook-form';
2+
3+
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
4+
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
5+
import { type SettingsDataModelFieldClickBehaviorFormValues } from '@/settings/data-model/fields/forms/utils/settingsDataModelFieldClickBehaviorSchema';
6+
import { Select } from '@/ui/input/components/Select';
7+
import { useLingui } from '@lingui/react/macro';
8+
import {
9+
FieldClickAction,
10+
type FieldMetadataMultiItemSettings,
11+
FieldMetadataType,
12+
} from 'twenty-shared/types';
13+
import { IconClick } from 'twenty-ui/display';
14+
15+
type SettingsDataModelFieldClickBehaviorFormProps = {
16+
disabled?: boolean;
17+
existingFieldMetadataId: string;
18+
fieldType: FieldMetadataType;
19+
};
20+
21+
export const SettingsDataModelFieldClickBehaviorForm = ({
22+
disabled,
23+
existingFieldMetadataId,
24+
fieldType,
25+
}: SettingsDataModelFieldClickBehaviorFormProps) => {
26+
const { t } = useLingui();
27+
const { control } =
28+
useFormContext<SettingsDataModelFieldClickBehaviorFormValues>();
29+
30+
const { fieldMetadataItem } = useFieldMetadataItemById(
31+
existingFieldMetadataId,
32+
);
33+
34+
let title: string;
35+
let description: string;
36+
let defaultValue: FieldClickAction;
37+
const options: Array<{ label: string; value: FieldClickAction }> = [];
38+
39+
switch (fieldType) {
40+
case FieldMetadataType.PHONES:
41+
title = t`Click Behavior`;
42+
description = t`Choose what happens when you click a phone number`;
43+
defaultValue = FieldClickAction.COPY;
44+
options.push(
45+
{ label: t`Copy to clipboard`, value: FieldClickAction.COPY },
46+
{ label: t`Open as link`, value: FieldClickAction.OPEN_LINK },
47+
);
48+
break;
49+
case FieldMetadataType.EMAILS:
50+
title = t`Click Behavior`;
51+
description = t`Choose what happens when you click an email`;
52+
defaultValue = FieldClickAction.OPEN_LINK;
53+
options.push(
54+
{ label: t`Open as link`, value: FieldClickAction.OPEN_LINK },
55+
{ label: t`Copy to clipboard`, value: FieldClickAction.COPY },
56+
);
57+
break;
58+
case FieldMetadataType.LINKS:
59+
title = t`Click Behavior`;
60+
description = t`Choose what happens when you click a link`;
61+
defaultValue = FieldClickAction.OPEN_LINK;
62+
options.push(
63+
{ label: t`Open as link`, value: FieldClickAction.OPEN_LINK },
64+
{ label: t`Copy to clipboard`, value: FieldClickAction.COPY },
65+
);
66+
break;
67+
default:
68+
return null;
69+
}
70+
71+
const existingSettings =
72+
(fieldMetadataItem?.settings as FieldMetadataMultiItemSettings) ?? {};
73+
74+
return (
75+
<Controller
76+
name="settings"
77+
control={control}
78+
defaultValue={{
79+
...existingSettings,
80+
clickAction: existingSettings.clickAction ?? defaultValue,
81+
}}
82+
render={({ field: { value, onChange } }) => {
83+
const currentSettings =
84+
(value as FieldMetadataMultiItemSettings | undefined) ?? {};
85+
86+
const clickAction = currentSettings.clickAction ?? defaultValue;
87+
88+
return (
89+
<SettingsOptionCardContentSelect
90+
Icon={IconClick}
91+
title={title}
92+
description={description}
93+
disabled={disabled}
94+
>
95+
<Select<FieldClickAction>
96+
dropdownWidth={180}
97+
value={clickAction}
98+
onChange={(newValue) =>
99+
onChange({
100+
...currentSettings,
101+
clickAction: newValue,
102+
})
103+
}
104+
disabled={disabled}
105+
dropdownId="field-click-behavior-select"
106+
options={options}
107+
selectSizeVariant="small"
108+
withSearchInput={false}
109+
/>
110+
</SettingsOptionCardContentSelect>
111+
);
112+
}}
113+
/>
114+
);
115+
};

packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { settingsDataModelFieldAddressFormSchema } from '@/settings/data-model/f
77
import { SettingsDataModelFieldAddressSettingsFormCard } from '@/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressSettingsFormCard';
88
import { settingsDataModelFieldBooleanFormSchema } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm';
99
import { SettingsDataModelFieldBooleanSettingsFormCard } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanSettingsFormCard';
10+
import { SettingsDataModelFieldClickBehaviorForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldClickBehaviorForm';
1011
import { SettingsDataModelFieldIsUniqueForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm';
1112
import { SettingsDataModelFieldMaxValuesForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldMaxValuesForm';
1213
import { settingsDataModelFieldTextFormSchema } from '@/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm';
@@ -26,6 +27,7 @@ import {
2627
} from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm';
2728
import { SettingsDataModelFieldSelectSettingsFormCard } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectSettingsFormCard';
2829
import { settingsDataModelFieldMaxValuesSchema } from '@/settings/data-model/fields/forms/utils/settingsDataModelFieldMaxValuesSchema';
30+
import { settingsDataModelFieldClickBehaviorSchema } from '@/settings/data-model/fields/forms/utils/settingsDataModelFieldClickBehaviorSchema';
2931
import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget';
3032

3133
import { Separator } from '@/settings/components/Separator';
@@ -92,11 +94,13 @@ const phonesFieldFormSchema = z
9294
const emailsFieldFormSchema = z
9395
.object({ type: z.literal(FieldMetadataType.EMAILS) })
9496
.extend(settingsDataModelFieldMaxValuesSchema.shape)
97+
.extend(settingsDataModelFieldClickBehaviorSchema.shape)
9598
.extend(isUniqueFieldFormSchema.shape);
9699

97100
const linksFieldFormSchema = z
98101
.object({ type: z.literal(FieldMetadataType.LINKS) })
99102
.extend(settingsDataModelFieldMaxValuesSchema.shape)
103+
.extend(settingsDataModelFieldClickBehaviorSchema.shape)
100104
.extend(isUniqueFieldFormSchema.shape);
101105

102106
const arrayFieldFormSchema = z
@@ -309,6 +313,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
309313
form={
310314
<>
311315
{[
316+
FieldMetadataType.PHONES,
312317
FieldMetadataType.EMAILS,
313318
FieldMetadataType.LINKS,
314319
FieldMetadataType.ARRAY,
@@ -322,6 +327,20 @@ export const SettingsDataModelFieldSettingsFormCard = ({
322327
<Separator />
323328
</>
324329
)}
330+
{[
331+
FieldMetadataType.PHONES,
332+
FieldMetadataType.EMAILS,
333+
FieldMetadataType.LINKS,
334+
].includes(fieldType) && (
335+
<>
336+
<SettingsDataModelFieldClickBehaviorForm
337+
existingFieldMetadataId={existingFieldMetadataId}
338+
fieldType={fieldType}
339+
disabled={disabled}
340+
/>
341+
<Separator />
342+
</>
343+
)}
325344
<SettingsDataModelFieldIsUniqueForm
326345
fieldType={fieldType}
327346
existingFieldMetadataId={existingFieldMetadataId}

0 commit comments

Comments
 (0)