Files
twenty/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutWidget.ts
T
Raphaël Bosi 5af2d37253 [DASHBOARDS] Dashboard duplication (#16291)
## Description

- Created a Dashboard duplication action
- Created a new duplication custom resolver
- Created the service using the v2 of the API
- Created the integration tests following the v2 methodology

## Video QA


https://github.com/user-attachments/assets/e409951a-5946-4da0-91a0-1f7d2ecadb08
2025-12-11 11:15:15 +00:00

151 lines
5.0 KiB
TypeScript

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 { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { generateDuplicatedTimestamps } from '@/page-layout/utils/generateDuplicatedTimestamps';
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 { appendCopySuffix, isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
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: appendCopySuffix(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 };
};