diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/CancelDashboardSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/CancelDashboardSingleRecordAction.tsx
index a4b2c7995b..3f6291977c 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/CancelDashboardSingleRecordAction.tsx
+++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/CancelDashboardSingleRecordAction.tsx
@@ -1,5 +1,6 @@
import { Action } from '@/action-menu/actions/components/Action';
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
+import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
@@ -12,6 +13,8 @@ export const CancelDashboardSingleRecordAction = () => {
const pageLayoutId = selectedRecord?.pageLayoutId;
+ const { closeCommandMenu } = useCommandMenu();
+
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
@@ -19,6 +22,8 @@ export const CancelDashboardSingleRecordAction = () => {
useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
const handleClick = () => {
+ closeCommandMenu();
+
resetDraftPageLayoutToPersistedPageLayout();
setIsPageLayoutInEditMode(false);
};
diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/SaveDashboardSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/SaveDashboardSingleRecordAction.tsx
index 05c2bd40a6..ba5bbc5a91 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/SaveDashboardSingleRecordAction.tsx
+++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/SaveDashboardSingleRecordAction.tsx
@@ -1,5 +1,6 @@
import { Action } from '@/action-menu/actions/components/Action';
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
+import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
@@ -17,8 +18,11 @@ export const SaveDashboardSingleRecordAction = () => {
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
+ const { closeCommandMenu } = useCommandMenu();
+
const handleClick = async () => {
await savePageLayout();
+ closeCommandMenu();
setIsPageLayoutInEditMode(false);
};
diff --git a/packages/twenty-front/src/modules/action-menu/components/CommandMenuActionMenuDropdown.tsx b/packages/twenty-front/src/modules/action-menu/components/CommandMenuActionMenuDropdown.tsx
index 90c6c2c5b9..387bea6a61 100644
--- a/packages/twenty-front/src/modules/action-menu/components/CommandMenuActionMenuDropdown.tsx
+++ b/packages/twenty-front/src/modules/action-menu/components/CommandMenuActionMenuDropdown.tsx
@@ -3,20 +3,9 @@ import { ActionScope } from '@/action-menu/actions/types/ActionScope';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId';
-import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
-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 { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown';
-import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
-import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
-import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
+import { OptionsDropdownMenu } from '@/ui/layout/dropdown/components/OptionsDropdownMenu';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
-import { useTheme } from '@emotion/react';
-import { t } from '@lingui/core/macro';
import { useContext } from 'react';
-import { Button } from 'twenty-ui/input';
-import { getOsControlSymbol } from 'twenty-ui/utilities';
export const CommandMenuActionMenuDropdown = () => {
const { actions } = useContext(ActionMenuContext);
@@ -25,31 +14,8 @@ export const CommandMenuActionMenuDropdown = () => {
ActionMenuComponentInstanceContext,
);
- const theme = useTheme();
-
const dropdownId =
getRightDrawerActionMenuDropdownIdFromActionMenuId(actionMenuId);
- const { toggleDropdown } = useToggleDropdown();
-
- const hotkeysConfig = {
- keys: ['ctrl+o', 'meta+o'],
- callback: () => {
- toggleDropdown({
- dropdownComponentInstanceIdFromProps: dropdownId,
- });
- },
- dependencies: [toggleDropdown],
- };
-
- useHotkeysOnFocusedElement({
- ...hotkeysConfig,
- focusId: SIDE_PANEL_FOCUS_ID,
- });
-
- useHotkeysOnFocusedElement({
- ...hotkeysConfig,
- focusId: dropdownId,
- });
const recordSelectionActions = actions.filter(
(action) => action.scope === ActionScope.RecordSelection,
@@ -59,43 +25,15 @@ export const CommandMenuActionMenuDropdown = () => {
(action) => action.key,
);
- const { setSelectedItemId } = useSelectableList(actionMenuId);
-
return (
-
- }
- dropdownPlacement="top-end"
- dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
- globalHotkeysConfig={{
- enableGlobalHotkeysWithModifiers: true,
- enableGlobalHotkeysConflictingWithKeyboard: false,
- }}
- onOpen={() => {
- setSelectedItemId(selectableItemIdArray[0]);
- }}
- dropdownComponents={
-
-
-
- {recordSelectionActions.map((action) => (
-
- ))}
-
-
-
- }
- />
+ selectableListId={actionMenuId}
+ selectableItemIdArray={selectableItemIdArray}
+ >
+ {recordSelectionActions.map((action) => (
+
+ ))}
+
);
};
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx
index 42a4d21586..cff403c647 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx
@@ -33,6 +33,13 @@ import {
type PageLayoutWidget,
} from '~/generated/graphql';
+const StyledCommandMenuContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+`;
+
const StyledSidePanelInformationBanner = styled(SidePanelInformationBanner)`
margin-top: ${({ theme }) => theme.spacing(2)};
`;
@@ -137,78 +144,80 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
primaryAxisField?.type === FieldMetadataType.DATE_TIME;
return (
-
-
- {hasWidgetTooManyGroups && (
-
+
+
- )}
- {chartSettings.map((group) => {
- const visibleItems = group.items.filter(
- (item) =>
- !shouldHideChartSetting(
- item,
- widget.objectMetadataId,
- isGroupByEnabled as boolean,
- configuration,
- objectMetadataItem,
- ),
- );
+ {hasWidgetTooManyGroups && (
+
+ )}
+ {chartSettings.map((group) => {
+ const visibleItems = group.items.filter(
+ (item) =>
+ !shouldHideChartSetting(
+ item,
+ widget.objectMetadataId,
+ isGroupByEnabled as boolean,
+ configuration,
+ objectMetadataItem,
+ ),
+ );
- return (
-
- {visibleItems.map((item) => {
- const handleItemToggleChange = () => {
- setSelectedItemId(item.id);
- updateChartSettingToggle(item.id);
- };
+ return (
+
+ {visibleItems.map((item) => {
+ const handleItemToggleChange = () => {
+ setSelectedItemId(item.id);
+ updateChartSettingToggle(item.id);
+ };
- const handleItemInputChange = (value: number | null) => {
- updateChartSettingInput(item.id, value);
- };
+ const handleItemInputChange = (value: number | null) => {
+ updateChartSettingInput(item.id, value);
+ };
- const handleItemDropdownOpen = () => {
- openDropdown({
- dropdownComponentInstanceIdFromProps: item.id,
- });
- };
+ const handleItemDropdownOpen = () => {
+ openDropdown({
+ dropdownComponentInstanceIdFromProps: item.id,
+ });
+ };
- const handleFilterClick = () => {
- navigatePageLayoutCommandMenu({
- commandMenuPage: CommandMenuPages.PageLayoutGraphFilter,
- });
- };
+ const handleFilterClick = () => {
+ navigatePageLayoutCommandMenu({
+ commandMenuPage: CommandMenuPages.PageLayoutGraphFilter,
+ });
+ };
- return (
-
- );
- })}
-
- );
- })}
-
+ return (
+
+ );
+ })}
+
+ );
+ })}
+
+
);
};
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx
index 2bd74d00ef..d187123cb6 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx
@@ -1,11 +1,19 @@
import { ChartSettings } from '@/command-menu/pages/page-layout/components/ChartSettings';
+import { WidgetSettingsFooter } from '@/command-menu/pages/page-layout/components/WidgetSettingsFooter';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { GraphWidgetComponentInstanceContext } from '@/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
+const StyledContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+`;
+
export const CommandMenuPageLayoutGraphTypeSelect = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
@@ -41,10 +49,13 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
}
return (
-
-
-
+
+
+
+
+
+
);
};
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/WidgetSettingsFooter.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/WidgetSettingsFooter.tsx
new file mode 100644
index 0000000000..d183e884db
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/WidgetSettingsFooter.tsx
@@ -0,0 +1,65 @@
+import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
+import { useDuplicatePageLayoutWidget } from '@/page-layout/hooks/useDuplicatePageLayoutWidget';
+import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
+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 { 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 { useLingui } from '@lingui/react/macro';
+import { useId } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+import { IconCopyPlus } from 'twenty-ui/display';
+import { MenuItem } from 'twenty-ui/navigation';
+
+export const WidgetSettingsFooter = () => {
+ const dropdownId = useId();
+ const { t } = useLingui();
+ const { closeDropdown } = useCloseDropdown();
+
+ const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
+ const { duplicateWidget } = useDuplicatePageLayoutWidget(pageLayoutId);
+
+ const editingWidgetId = useRecoilComponentValue(
+ pageLayoutEditingWidgetIdComponentState,
+ pageLayoutId,
+ );
+
+ const handleDuplicateWidget = () => {
+ if (isDefined(editingWidgetId)) {
+ duplicateWidget(editingWidgetId);
+ }
+ closeDropdown(dropdownId);
+ };
+
+ const selectedItemId = useRecoilComponentValue(
+ selectedItemIdComponentState,
+ dropdownId,
+ );
+
+ return (
+
+
+
+
+ ,
+ ]}
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx
index fc542cbf6f..aa69e31889 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx
@@ -7,6 +7,8 @@ import {
PAGE_LAYOUT_CONFIG,
type PageLayoutBreakpoint,
} from '@/page-layout/constants/PageLayoutBreakpoints';
+import { PAGE_LAYOUT_GRID_MARGIN } from '@/page-layout/constants/PageLayoutGridMargin';
+import { PAGE_LAYOUT_GRID_ROW_HEIGHT } from '@/page-layout/constants/PageLayoutGridRowHeight';
import { usePageLayoutHandleLayoutChange } from '@/page-layout/hooks/usePageLayoutHandleLayoutChange';
import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
@@ -153,10 +155,10 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
layouts={layouts}
breakpoints={PAGE_LAYOUT_CONFIG.breakpoints}
cols={PAGE_LAYOUT_CONFIG.columns}
- rowHeight={55}
+ rowHeight={PAGE_LAYOUT_GRID_ROW_HEIGHT}
maxCols={12}
containerPadding={[0, 0]}
- margin={[8, 8]}
+ margin={[PAGE_LAYOUT_GRID_MARGIN, PAGE_LAYOUT_GRID_MARGIN]}
isDraggable={isPageLayoutInEditMode}
isResizable={isPageLayoutInEditMode}
draggableHandle=".drag-handle"
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx
index cd632779f4..65c26fa822 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx
@@ -10,6 +10,7 @@ import { useReorderPageLayoutTabs } from '@/page-layout/hooks/useReorderPageLayo
import { PageLayoutMainContent } from '@/page-layout/PageLayoutMainContent';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
+import { getScrollWrapperInstanceIdFromPageLayoutId } from '@/page-layout/utils/getScrollWrapperInstanceIdFromPageLayoutId';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { getTabsByDisplayMode } from '@/page-layout/utils/getTabsByDisplayMode';
import { getTabsWithVisibleWidgets } from '@/page-layout/utils/getTabsWithVisibleWidgets';
@@ -133,7 +134,9 @@ export const PageLayoutRendererContent = () => {
{isDefined(activeTabId) && (
diff --git a/packages/twenty-front/src/modules/page-layout/constants/PageLayoutGridMargin.ts b/packages/twenty-front/src/modules/page-layout/constants/PageLayoutGridMargin.ts
new file mode 100644
index 0000000000..3b6f171256
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/constants/PageLayoutGridMargin.ts
@@ -0,0 +1 @@
+export const PAGE_LAYOUT_GRID_MARGIN = 8;
diff --git a/packages/twenty-front/src/modules/page-layout/constants/PageLayoutGridRowHeight.ts b/packages/twenty-front/src/modules/page-layout/constants/PageLayoutGridRowHeight.ts
new file mode 100644
index 0000000000..77934b991f
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/constants/PageLayoutGridRowHeight.ts
@@ -0,0 +1 @@
+export const PAGE_LAYOUT_GRID_ROW_HEIGHT = 55;
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutTab.ts b/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutTab.ts
index 45ae2ef860..7e2357d163 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutTab.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutTab.ts
@@ -16,6 +16,8 @@ import { pageLayoutCurrentLayoutsComponentState } from '../states/pageLayoutCurr
import { pageLayoutDraftComponentState } from '../states/pageLayoutDraftComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '../states/pageLayoutTabSettingsOpenTabIdComponentState';
import { type PageLayoutTab } from '../types/PageLayoutTab';
+import { generateDuplicatedTimestamps } from '../utils/generateDuplicatedTimestamps';
+import { getDuplicatedTitle } from '../utils/getDuplicatedTitle';
export const useDuplicatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -76,8 +78,7 @@ export const useDuplicatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
...widget,
id: newWidgetId,
pageLayoutTabId: newTabId,
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
+ ...generateDuplicatedTimestamps(),
};
});
@@ -93,13 +94,10 @@ export const useDuplicatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
const newTab: PageLayoutTab = {
...sourceTab,
id: newTabId,
- title: sourceTab.title.endsWith('(Copy)')
- ? sourceTab.title
- : `${sourceTab.title} (Copy)`,
+ title: getDuplicatedTitle(sourceTab.title),
position: newTabPosition,
widgets: clonedWidgets,
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
+ ...generateDuplicatedTimestamps(),
};
const sourceLayouts = allTabLayouts[tabId] ?? {
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutWidget.ts
new file mode 100644
index 0000000000..9a5020a55c
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutWidget.ts
@@ -0,0 +1,151 @@
+import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
+import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
+import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
+import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
+import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
+import { getScrollWrapperInstanceIdFromPageLayoutId } from '@/page-layout/utils/getScrollWrapperInstanceIdFromPageLayoutId';
+import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
+import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
+import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
+import { useRecoilCallback } from 'recoil';
+import { isDefined } from 'twenty-shared/utils';
+import { v4 as uuidv4 } from 'uuid';
+import { type PageLayoutWidget } from '~/generated/graphql';
+import { generateDuplicatedTimestamps } from '../utils/generateDuplicatedTimestamps';
+import { getDuplicatedTitle } from '../utils/getDuplicatedTitle';
+
+export const useDuplicatePageLayoutWidget = (
+ pageLayoutIdFromProps?: string,
+) => {
+ const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
+ PageLayoutComponentInstanceContext,
+ pageLayoutIdFromProps,
+ );
+
+ const pageLayoutDraftState = useRecoilComponentCallbackState(
+ pageLayoutDraftComponentState,
+ pageLayoutId,
+ );
+
+ const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
+ pageLayoutCurrentLayoutsComponentState,
+ pageLayoutId,
+ );
+
+ const setEditingWidgetId = useSetRecoilComponentState(
+ pageLayoutEditingWidgetIdComponentState,
+ pageLayoutId,
+ );
+
+ const { getScrollWrapperElement } = useScrollWrapperHTMLElement(
+ getScrollWrapperInstanceIdFromPageLayoutId(pageLayoutId),
+ );
+
+ const duplicateWidget = useRecoilCallback(
+ ({ snapshot, set }) =>
+ (widgetId: string): string => {
+ const pageLayoutDraft = snapshot
+ .getLoadable(pageLayoutDraftState)
+ .getValue();
+
+ const allTabLayouts = snapshot
+ .getLoadable(pageLayoutCurrentLayoutsState)
+ .getValue();
+
+ const sourceWidget = pageLayoutDraft.tabs
+ .flatMap((tab) => tab.widgets)
+ .find((widget) => widget.id === widgetId);
+
+ if (!isDefined(sourceWidget)) {
+ throw new Error(`Widget with id ${widgetId} not found`);
+ }
+
+ const sourceTab = pageLayoutDraft.tabs.find(
+ (tab) => tab.id === sourceWidget.pageLayoutTabId,
+ );
+
+ if (!isDefined(sourceTab)) {
+ throw new Error(
+ `Tab with id ${sourceWidget.pageLayoutTabId} not found`,
+ );
+ }
+
+ const newWidgetId = uuidv4();
+
+ const clonedWidget: PageLayoutWidget = {
+ ...sourceWidget,
+ id: newWidgetId,
+ title: getDuplicatedTitle(sourceWidget.title),
+ ...generateDuplicatedTimestamps(),
+ };
+
+ const currentTabLayouts = allTabLayouts[sourceTab.id] || {
+ desktop: [],
+ mobile: [],
+ };
+
+ const sourceLayout = currentTabLayouts.desktop.find(
+ (layout) => layout.i === widgetId,
+ );
+
+ if (!isDefined(sourceLayout)) {
+ throw new Error(`Layout for widget ${widgetId} not found`);
+ }
+
+ const maxY = currentTabLayouts.desktop.reduce(
+ (max, layout) => Math.max(max, layout.y + layout.h),
+ 0,
+ );
+
+ const newLayout = {
+ ...sourceLayout,
+ i: newWidgetId,
+ y: maxY,
+ };
+
+ const updatedLayouts = getUpdatedTabLayouts(
+ allTabLayouts,
+ sourceTab.id,
+ newLayout,
+ );
+
+ set(pageLayoutCurrentLayoutsState, updatedLayouts);
+
+ set(pageLayoutDraftState, (prev) => ({
+ ...prev,
+ tabs: addWidgetToTab(prev.tabs, sourceTab.id, clonedWidget),
+ }));
+
+ setEditingWidgetId(newWidgetId);
+
+ const { scrollWrapperElement } = getScrollWrapperElement();
+
+ if (isDefined(scrollWrapperElement)) {
+ requestAnimationFrame(() => {
+ const widgetElement = scrollWrapperElement.querySelector(
+ `[data-widget-id="${newWidgetId}"]`,
+ );
+
+ if (isDefined(widgetElement)) {
+ widgetElement.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ });
+ }
+ });
+ }
+
+ return newWidgetId;
+ },
+ [
+ pageLayoutCurrentLayoutsState,
+ pageLayoutDraftState,
+ setEditingWidgetId,
+ getScrollWrapperElement,
+ ],
+ );
+
+ return { duplicateWidget };
+};
diff --git a/packages/twenty-front/src/modules/page-layout/utils/generateDuplicatedTimestamps.ts b/packages/twenty-front/src/modules/page-layout/utils/generateDuplicatedTimestamps.ts
new file mode 100644
index 0000000000..e214d683a2
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/utils/generateDuplicatedTimestamps.ts
@@ -0,0 +1,11 @@
+export const generateDuplicatedTimestamps = (): {
+ createdAt: string;
+ updatedAt: string;
+} => {
+ const now = new Date().toISOString();
+
+ return {
+ createdAt: now,
+ updatedAt: now,
+ };
+};
diff --git a/packages/twenty-front/src/modules/page-layout/utils/getDuplicatedTitle.ts b/packages/twenty-front/src/modules/page-layout/utils/getDuplicatedTitle.ts
new file mode 100644
index 0000000000..1e6360a0ad
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/utils/getDuplicatedTitle.ts
@@ -0,0 +1,3 @@
+export const getDuplicatedTitle = (title: string): string => {
+ return title.endsWith('(Copy)') ? title : `${title} (Copy)`;
+};
diff --git a/packages/twenty-front/src/modules/page-layout/utils/getScrollWrapperInstanceIdFromPageLayoutId.ts b/packages/twenty-front/src/modules/page-layout/utils/getScrollWrapperInstanceIdFromPageLayoutId.ts
new file mode 100644
index 0000000000..ac90e1e27f
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/utils/getScrollWrapperInstanceIdFromPageLayoutId.ts
@@ -0,0 +1,5 @@
+export const getScrollWrapperInstanceIdFromPageLayoutId = (
+ pageLayoutId: string,
+) => {
+ return `scroll-wrapper-page-layout-${pageLayoutId}`;
+};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx
index fb2b80c627..4c6765e38e 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx
@@ -103,6 +103,7 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
isResizing={isResizing}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
+ data-widget-id={widget.id}
>
{showHeader && (
void;
+ children: ReactNode;
+};
+
+export const OptionsDropdownMenu = ({
+ dropdownId: dropdownIdFromProps,
+ selectableListId,
+ selectableItemIdArray = [],
+ onOpen,
+ children,
+}: OptionsDropdownMenuProps) => {
+ const generatedDropdownId = useId();
+ const dropdownId = dropdownIdFromProps ?? generatedDropdownId;
+ const { t } = useLingui();
+ const theme = useTheme();
+ const { toggleDropdown } = useToggleDropdown();
+
+ const listId = selectableListId ?? dropdownId;
+ const { setSelectedItemId } = useSelectableList(listId);
+
+ const handleOpen = () => {
+ if (selectableItemIdArray.length > 0) {
+ setSelectedItemId(selectableItemIdArray[0]);
+ }
+ onOpen?.();
+ };
+
+ const hotkeysConfig = {
+ keys: ['ctrl+o', 'meta+o'],
+ callback: () => {
+ toggleDropdown({
+ dropdownComponentInstanceIdFromProps: dropdownId,
+ });
+ },
+ dependencies: [toggleDropdown, dropdownId],
+ };
+
+ useHotkeysOnFocusedElement({
+ ...hotkeysConfig,
+ focusId: SIDE_PANEL_FOCUS_ID,
+ });
+
+ useHotkeysOnFocusedElement({
+ ...hotkeysConfig,
+ focusId: dropdownId,
+ });
+
+ return (
+
+ }
+ dropdownPlacement="top-end"
+ dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
+ globalHotkeysConfig={{
+ enableGlobalHotkeysWithModifiers: true,
+ enableGlobalHotkeysConflictingWithKeyboard: false,
+ }}
+ onOpen={handleOpen}
+ dropdownComponents={
+
+
+
+ {children}
+
+
+
+ }
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepFooter.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepFooter.tsx
index 4a507d9004..50643f8a5c 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepFooter.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepFooter.tsx
@@ -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 = (
-
+
+
- }
- dropdownPlacement="top-end"
- dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
- globalHotkeysConfig={{
- enableGlobalHotkeysWithModifiers: true,
- enableGlobalHotkeysConflictingWithKeyboard: false,
- }}
- dropdownComponents={
-
-
-
-
-
-
- }
- />
+
+ {stepId !== TRIGGER_STEP_ID && (
+
+
+
+ )}
+ {!shouldPinDeleteButton && (
+
+
+
+ )}
+
);
const deleteButton = (