[DASHBOARDS] Widget duplication (#15979)

## Widget duplication

- Created a shared component for the option menu shared by the record
page, the dashboards and the workflows
- Fix arrow selection in the option menu in workflows, which wasn't
working
- Scroll to the new widget after creation and open the new widget
settings


https://github.com/user-attachments/assets/47b8dade-44bd-4ba2-a81c-01b09e8718d3
This commit is contained in:
Raphaël Bosi
2025-11-24 14:38:18 +01:00
committed by GitHub
parent 8923d2fa0a
commit 2b80d9e015
18 changed files with 526 additions and 217 deletions
@@ -1,14 +1,13 @@
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
import { useCommandMenuWorkflowIdOrThrow } from '@/command-menu/pages/workflow/hooks/useCommandMenuWorkflowIdOrThrow';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { OptionsDropdownMenu } from '@/ui/layout/dropdown/components/OptionsDropdownMenu';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
import { useDuplicateStep } from '@/workflow/workflow-steps/hooks/useDuplicateStep';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useId } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -16,7 +15,6 @@ import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { IconCopyPlus, IconPencil, IconTrash } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { getOsControlSymbol } from 'twenty-ui/utilities';
export const WorkflowStepFooter = ({
stepId,
@@ -27,7 +25,6 @@ export const WorkflowStepFooter = ({
}) => {
const dropdownId = useId();
const { t } = useLingui();
const theme = useTheme();
const { duplicateStep } = useDuplicateStep();
const { closeDropdown } = useCloseDropdown();
const workflowId = useCommandMenuWorkflowIdOrThrow();
@@ -39,66 +36,75 @@ export const WorkflowStepFooter = ({
const shouldPinDeleteButton =
!isDefined(additionalActions) || additionalActions.length === 0;
const selectableItemIdArray = [
'change-node-type',
...(stepId !== TRIGGER_STEP_ID ? ['duplicate'] : []),
...(!shouldPinDeleteButton ? ['delete'] : []),
];
const handleChangeNodeType = () => {
closeDropdown(dropdownId);
if (stepId === TRIGGER_STEP_ID) {
openWorkflowTriggerTypeInCommandMenu(workflowId);
} else {
openWorkflowEditStepTypeInCommandMenu(workflowId);
}
};
const handleDuplicateNode = () => {
closeDropdown(dropdownId);
duplicateStep({ stepId });
};
const handleDeleteNode = () => {
closeDropdown(dropdownId);
deleteStep(stepId);
};
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
dropdownId,
);
const OptionsDropdown = (
<Dropdown
<OptionsDropdownMenu
dropdownId={dropdownId}
data-select-disable
clickableComponent={
<Button
size="small"
title={t`Options`}
hotkeys={[getOsControlSymbol(), 'O']}
selectableListId={dropdownId}
selectableItemIdArray={selectableItemIdArray}
>
<SelectableListItem
itemId="change-node-type"
onEnter={handleChangeNodeType}
>
<MenuItem
focused={selectedItemId === 'change-node-type'}
onClick={handleChangeNodeType}
text={t`Change node type`}
LeftIcon={IconPencil}
/>
}
dropdownPlacement="top-end"
dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
globalHotkeysConfig={{
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: false,
}}
dropdownComponents={
<DropdownContent>
<DropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={dropdownId}
focusId={dropdownId}
selectableItemIdArray={['change-node-type', 'duplicate']}
>
<MenuItem
onClick={() => {
closeDropdown(dropdownId);
stepId === TRIGGER_STEP_ID
? openWorkflowTriggerTypeInCommandMenu(workflowId)
: openWorkflowEditStepTypeInCommandMenu(workflowId);
}}
text={t`Change node type`}
LeftIcon={IconPencil}
/>
{stepId !== TRIGGER_STEP_ID && (
<MenuItem
onClick={() => {
closeDropdown(dropdownId);
duplicateStep({ stepId });
}}
text={t`Duplicate node`}
LeftIcon={IconCopyPlus}
/>
)}
{!shouldPinDeleteButton && (
<MenuItem
onClick={() => {
closeDropdown(dropdownId);
deleteStep(stepId);
}}
text={t`Delete node`}
LeftIcon={IconTrash}
/>
)}
</SelectableList>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
</SelectableListItem>
{stepId !== TRIGGER_STEP_ID && (
<SelectableListItem itemId="duplicate" onEnter={handleDuplicateNode}>
<MenuItem
focused={selectedItemId === 'duplicate'}
onClick={handleDuplicateNode}
text={t`Duplicate node`}
LeftIcon={IconCopyPlus}
/>
</SelectableListItem>
)}
{!shouldPinDeleteButton && (
<SelectableListItem itemId="delete" onEnter={handleDeleteNode}>
<MenuItem
focused={selectedItemId === 'delete'}
onClick={handleDeleteNode}
text={t`Delete node`}
LeftIcon={IconTrash}
/>
</SelectableListItem>
)}
</OptionsDropdownMenu>
);
const deleteButton = (