From d43e21b695987c9c35e434355435d3ad725a18e8 Mon Sep 17 00:00:00 2001 From: Lakshay Manchanda Date: Wed, 17 Dec 2025 00:13:00 -0800 Subject: [PATCH] Feature 15797 add secondary action button (#16582) This is a fix for #15797 This pull request is to replace PR #16307 and to extend #16265 Just to repeat, this PR does the following -> **Table Cell Button and Edit Button Improvements** - Enhanced RecordTableCellButton to support a secondary action and icon, enabling both the primary and secondary actions based on the selected action mode. - Updated RecordTableCellEditButton to determine the action mode for actionable fields, and provide both copy and navigate actions as primary/secondary buttons, with appropriate feedback. ## When primary function is to copy image ## When primary function is to open link image Hey @etiennejouan, please have a look! Thank you --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> --- .../components/RecordTableCellButton.tsx | 29 ------ .../components/RecordTableCellButtons.tsx | 34 +++++++ .../components/RecordTableCellEditButton.tsx | 22 +++-- .../useGetSecondaryRecordTableCellButton.ts | 88 +++++++++++++++++++ ...ngsDataModelFieldPreviewWidget.stories.tsx | 1 + 5 files changed, 139 insertions(+), 35 deletions(-) delete mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButtons.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useGetSecondaryRecordTableCellButton.ts diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx deleted file mode 100644 index 8cceeaab74..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import styled from '@emotion/styled'; -import { AnimatedContainer } from 'twenty-ui/utilities'; -import { FloatingIconButton } from 'twenty-ui/input'; -import { type IconComponent } from 'twenty-ui/display'; -import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; - -const StyledButtonContainer = styled.div` - margin: ${({ theme }) => theme.spacing(1)}; - @media (max-width: ${MOBILE_VIEWPORT}px) { - position: relative; - right: 7px; - } -`; - -type RecordTableCellButtonProps = { - onClick?: () => void; - Icon: IconComponent; -}; - -export const RecordTableCellButton = ({ - onClick, - Icon, -}: RecordTableCellButtonProps) => ( - - - - - -); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButtons.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButtons.tsx new file mode 100644 index 0000000000..2d1b5cc09b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButtons.tsx @@ -0,0 +1,34 @@ +import styled from '@emotion/styled'; +import { type IconComponent } from 'twenty-ui/display'; +import { LightIconButtonGroup } from 'twenty-ui/input'; +import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; +import { AnimatedContainer } from 'twenty-ui/utilities'; + +const StyledButtonContainer = styled.div` + margin: ${({ theme }) => theme.spacing(1)}; + @media (max-width: ${MOBILE_VIEWPORT}px) { + position: relative; + right: 7px; + } + border-radius: ${({ theme }) => theme.border.radius.sm}; + border: 1px solid ${({ theme }) => theme.border.color.strong}; +`; + +type RecordTableCellButtonsProps = { + onClick?: () => void; + Icon: IconComponent; +}[]; + +export const RecordTableCellButtons = ({ + buttons, +}: { + buttons: RecordTableCellButtonsProps; +}) => { + return ( + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditButton.tsx index a996c1a009..fc362d1c7b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditButton.tsx @@ -1,7 +1,9 @@ import { useGetButtonIcon } from '@/object-record/record-field/ui/hooks/useGetButtonIcon'; import { useIsFieldInputOnly } from '@/object-record/record-field/ui/hooks/useIsFieldInputOnly'; + import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; -import { RecordTableCellButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellButton'; +import { RecordTableCellButtons } from '@/object-record/record-table/record-table-cell/components/RecordTableCellButtons'; +import { useGetSecondaryRecordTableCellButton } from '@/object-record/record-table/record-table-cell/hooks/useGetSecondaryRecordTableCellButton'; import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell'; import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; @@ -9,20 +11,20 @@ import { IconArrowUpRight, IconPencil } from 'twenty-ui/display'; export const RecordTableCellEditButton = () => { const { cellPosition } = useContext(RecordTableCellContext); - const { openTableCell } = useOpenRecordTableCellFromCell(); - const isFieldInputOnly = useIsFieldInputOnly(); const isFirstColumn = cellPosition.column === 0; const customButtonIcon = useGetButtonIcon(); - const buttonIcon = isFirstColumn + const secondaryButton = useGetSecondaryRecordTableCellButton(); + + const mainButtonIcon = isFirstColumn ? IconArrowUpRight : isDefined(customButtonIcon) ? customButtonIcon : IconPencil; - const handleButtonClick = () => { + const handleMainButtonClick = () => { if (!isFieldInputOnly && isFirstColumn) { openTableCell(undefined, false, true); } else { @@ -31,6 +33,14 @@ export const RecordTableCellEditButton = () => { }; return ( - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useGetSecondaryRecordTableCellButton.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useGetSecondaryRecordTableCellButton.ts new file mode 100644 index 0000000000..3cfa98a335 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useGetSecondaryRecordTableCellButton.ts @@ -0,0 +1,88 @@ +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { + type FieldEmailsValue, + type FieldLinksValue, + type FieldPhonesValue, +} from '@/object-record/record-field/ui/types/FieldMetadata'; +import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFieldEmails'; +import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks'; +import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones'; +import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue'; +import { t } from '@lingui/core/macro'; +import { useContext } from 'react'; +import { FieldMetadataSettingsOnClickAction } from 'twenty-shared/types'; +import { getAbsoluteUrl, isDefined } from 'twenty-shared/utils'; +import { IconArrowUpRight, IconCopy } from 'twenty-ui/display'; +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; + +export const useGetSecondaryRecordTableCellButton = () => { + const { fieldDefinition, recordId } = useContext(FieldContext); + const { copyToClipboard } = useCopyToClipboard(); + + const fieldValue = useRecordFieldValue< + FieldPhonesValue | FieldEmailsValue | FieldLinksValue | undefined + >(recordId, fieldDefinition.metadata.fieldName, fieldDefinition); + + if ( + (!isFieldPhones(fieldDefinition) && + !isFieldLinks(fieldDefinition) && + !isFieldEmails(fieldDefinition)) || + !isDefined(fieldValue) + ) { + return []; + } + + const secondaryActionOnClick = + fieldDefinition.metadata.settings?.clickAction === + FieldMetadataSettingsOnClickAction.OPEN_LINK + ? FieldMetadataSettingsOnClickAction.COPY + : FieldMetadataSettingsOnClickAction.OPEN_LINK; + + let openLinkOnClick: () => void = () => {}; + let copyOnClick: () => void = () => {}; + + if (isFieldPhones(fieldDefinition)) { + const { primaryPhoneCallingCode = '', primaryPhoneNumber = '' } = + fieldValue as FieldPhonesValue; + const phoneNumber = `${primaryPhoneCallingCode}${primaryPhoneNumber}`; + openLinkOnClick = () => { + window.open(`tel:${phoneNumber}`, '_blank'); + }; + copyOnClick = () => { + copyToClipboard(phoneNumber, t`Phone number copied to clipboard`); + }; + } + + if (isFieldEmails(fieldDefinition)) { + const email = (fieldValue as FieldEmailsValue).primaryEmail ?? ''; + openLinkOnClick = () => { + window.open(`mailto:${email}`, '_blank'); + }; + copyOnClick = () => { + copyToClipboard(email, t`Email copied to clipboard`); + }; + } + + if (isFieldLinks(fieldDefinition)) { + const url = (fieldValue as FieldLinksValue).primaryLinkUrl ?? ''; + openLinkOnClick = () => { + window.open(getAbsoluteUrl(url), '_blank'); + }; + copyOnClick = () => { + copyToClipboard(url, t`Link copied to clipboard`); + }; + } + + return [ + { + onClick: + secondaryActionOnClick === FieldMetadataSettingsOnClickAction.OPEN_LINK + ? openLinkOnClick + : copyOnClick, + Icon: + secondaryActionOnClick === FieldMetadataSettingsOnClickAction.OPEN_LINK + ? IconArrowUpRight + : IconCopy, + }, + ]; +}; 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 16400bb1a8..a363fe51b1 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 @@ -30,6 +30,7 @@ const meta: Meta = { MemoryRouterDecorator, ComponentDecorator, ObjectMetadataItemsDecorator, + I18nFrontDecorator, SnackBarDecorator, ], args: {