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 <img width="1817" height="939" alt="image" src="https://github.com/user-attachments/assets/7ec6c6aa-80d8-402b-a210-519163d39ef6" /> ## When primary function is to open link <img width="1784" height="942" alt="image" src="https://github.com/user-attachments/assets/dfe0fcf1-ba72-4083-a5f9-7165a03db3df" /> Hey @etiennejouan, please have a look! Thank you --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0145920d7e
commit
d43e21b695
-29
@@ -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) => (
|
||||
<AnimatedContainer>
|
||||
<StyledButtonContainer>
|
||||
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
|
||||
</StyledButtonContainer>
|
||||
</AnimatedContainer>
|
||||
);
|
||||
+34
@@ -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 (
|
||||
<AnimatedContainer>
|
||||
<StyledButtonContainer>
|
||||
<LightIconButtonGroup size="small" iconButtons={buttons} />
|
||||
</StyledButtonContainer>
|
||||
</AnimatedContainer>
|
||||
);
|
||||
};
|
||||
+16
-6
@@ -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 (
|
||||
<RecordTableCellButton onClick={handleButtonClick} Icon={buttonIcon} />
|
||||
<RecordTableCellButtons
|
||||
buttons={[
|
||||
...secondaryButton,
|
||||
{
|
||||
onClick: handleMainButtonClick,
|
||||
Icon: mainButtonIcon,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+88
@@ -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,
|
||||
},
|
||||
];
|
||||
};
|
||||
+1
@@ -30,6 +30,7 @@ const meta: Meta<typeof SettingsDataModelFieldPreviewWidget> = {
|
||||
MemoryRouterDecorator,
|
||||
ComponentDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
I18nFrontDecorator,
|
||||
SnackBarDecorator,
|
||||
],
|
||||
args: {
|
||||
|
||||
Reference in New Issue
Block a user