copy to clipboard on MultiItemFieldMenuItem (#13292)

# ISSUE

- closes #13089
This commit is contained in:
Nabhag Motivaras
2025-07-23 15:13:16 +05:30
committed by GitHub
parent ac8dab279a
commit 2730b3ea3d
4 changed files with 37 additions and 0 deletions

View File

@ -6,6 +6,7 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
import { MultiItemFieldInput } from './MultiItemFieldInput'; import { MultiItemFieldInput } from './MultiItemFieldInput';
type EmailsFieldInputProps = { type EmailsFieldInputProps = {
@ -18,6 +19,7 @@ export const EmailsFieldInput = ({
onClickOutside, onClickOutside,
}: EmailsFieldInputProps) => { }: EmailsFieldInputProps) => {
const { persistEmailsField, fieldValue } = useEmailsField(); const { persistEmailsField, fieldValue } = useEmailsField();
const { copyToClipboard } = useCopyToClipboard();
const emails = useMemo<string[]>( const emails = useMemo<string[]>(
() => () =>
@ -56,6 +58,10 @@ export const EmailsFieldInput = ({
setIsFieldInError(hasError && values.length === 0); setIsFieldInError(hasError && values.length === 0);
}; };
const handleCopy = (email: string) => {
copyToClipboard(email);
};
return ( return (
<MultiItemFieldInput <MultiItemFieldInput
items={emails} items={emails}
@ -80,10 +86,12 @@ export const EmailsFieldInput = ({
dropdownId={`emails-${index}`} dropdownId={`emails-${index}`}
showPrimaryIcon={getShowPrimaryIcon(index)} showPrimaryIcon={getShowPrimaryIcon(index)}
showSetAsPrimaryButton={getShowSetAsPrimaryButton(index)} showSetAsPrimaryButton={getShowSetAsPrimaryButton(index)}
showCopyButton={true}
email={email} email={email}
onEdit={handleEdit} onEdit={handleEdit}
onSetAsPrimary={handleSetPrimary} onSetAsPrimary={handleSetPrimary}
onDelete={handleDelete} onDelete={handleDelete}
onCopy={handleCopy}
/> />
)} )}
onError={handleError} onError={handleError}

View File

@ -6,9 +6,11 @@ type EmailsFieldMenuItemProps = {
onEdit?: () => void; onEdit?: () => void;
onSetAsPrimary?: () => void; onSetAsPrimary?: () => void;
onDelete?: () => void; onDelete?: () => void;
onCopy?: (email: string) => void;
email: string; email: string;
showPrimaryIcon: boolean; showPrimaryIcon: boolean;
showSetAsPrimaryButton: boolean; showSetAsPrimaryButton: boolean;
showCopyButton: boolean;
}; };
export const EmailsFieldMenuItem = ({ export const EmailsFieldMenuItem = ({
@ -19,6 +21,8 @@ export const EmailsFieldMenuItem = ({
email, email,
showPrimaryIcon, showPrimaryIcon,
showSetAsPrimaryButton, showSetAsPrimaryButton,
showCopyButton,
onCopy,
}: EmailsFieldMenuItemProps) => { }: EmailsFieldMenuItemProps) => {
return ( return (
<MultiItemFieldMenuItem <MultiItemFieldMenuItem
@ -30,6 +34,8 @@ export const EmailsFieldMenuItem = ({
DisplayComponent={EmailDisplay} DisplayComponent={EmailDisplay}
showPrimaryIcon={showPrimaryIcon} showPrimaryIcon={showPrimaryIcon}
showSetAsPrimaryButton={showSetAsPrimaryButton} showSetAsPrimaryButton={showSetAsPrimaryButton}
showCopyButton={showCopyButton}
onCopy={onCopy}
/> />
); );
}; };

View File

@ -8,6 +8,7 @@ import React, { useState } from 'react';
import { import {
IconBookmark, IconBookmark,
IconBookmarkPlus, IconBookmarkPlus,
IconCopy,
IconPencil, IconPencil,
IconTrash, IconTrash,
} from 'twenty-ui/display'; } from 'twenty-ui/display';
@ -19,9 +20,11 @@ type MultiItemFieldMenuItemProps<T> = {
onEdit?: () => void; onEdit?: () => void;
onSetAsPrimary?: () => void; onSetAsPrimary?: () => void;
onDelete?: () => void; onDelete?: () => void;
onCopy?: (value: T) => void;
DisplayComponent: React.ComponentType<{ value: T }>; DisplayComponent: React.ComponentType<{ value: T }>;
showPrimaryIcon: boolean; showPrimaryIcon: boolean;
showSetAsPrimaryButton: boolean; showSetAsPrimaryButton: boolean;
showCopyButton?: boolean;
}; };
export const MultiItemFieldMenuItem = <T,>({ export const MultiItemFieldMenuItem = <T,>({
@ -33,6 +36,8 @@ export const MultiItemFieldMenuItem = <T,>({
DisplayComponent, DisplayComponent,
showPrimaryIcon, showPrimaryIcon,
showSetAsPrimaryButton, showSetAsPrimaryButton,
showCopyButton,
onCopy,
}: MultiItemFieldMenuItemProps<T>) => { }: MultiItemFieldMenuItemProps<T>) => {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const { closeDropdown } = useCloseDropdown(); const { closeDropdown } = useCloseDropdown();
@ -71,6 +76,14 @@ export const MultiItemFieldMenuItem = <T,>({
onEdit?.(); onEdit?.();
}; };
const handleCopyClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
event.preventDefault();
closeDropdown(dropdownId);
onCopy?.(value);
};
return ( return (
<MenuItemWithOptionDropdown <MenuItemWithOptionDropdown
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
@ -100,6 +113,13 @@ export const MultiItemFieldMenuItem = <T,>({
text="Delete" text="Delete"
onClick={handleDeleteClick} onClick={handleDeleteClick}
/> />
{showCopyButton && (
<MenuItem
LeftIcon={IconCopy}
text="Copy"
onClick={handleCopyClick}
/>
)}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</DropdownContent> </DropdownContent>
} }

View File

@ -13,6 +13,8 @@ import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFi
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { EmailsFieldInput } from '../EmailsFieldInput'; import { EmailsFieldInput } from '../EmailsFieldInput';
const updateRecord = fn(); const updateRecord = fn();
@ -113,6 +115,7 @@ const EmailInputWithContext = ({
const meta: Meta<typeof EmailInputWithContext> = { const meta: Meta<typeof EmailInputWithContext> = {
title: 'UI/Input/EmailsFieldInput', title: 'UI/Input/EmailsFieldInput',
component: EmailInputWithContext, component: EmailInputWithContext,
decorators: [SnackBarDecorator, I18nFrontDecorator],
}; };
export default meta; export default meta;