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={ - - - - { - closeDropdown(dropdownId); - stepId === TRIGGER_STEP_ID - ? openWorkflowTriggerTypeInCommandMenu(workflowId) - : openWorkflowEditStepTypeInCommandMenu(workflowId); - }} - text={t`Change node type`} - LeftIcon={IconPencil} - /> - {stepId !== TRIGGER_STEP_ID && ( - { - closeDropdown(dropdownId); - duplicateStep({ stepId }); - }} - text={t`Duplicate node`} - LeftIcon={IconCopyPlus} - /> - )} - {!shouldPinDeleteButton && ( - { - closeDropdown(dropdownId); - deleteStep(stepId); - }} - text={t`Delete node`} - LeftIcon={IconTrash} - /> - )} - - - - } - /> + + {stepId !== TRIGGER_STEP_ID && ( + + + + )} + {!shouldPinDeleteButton && ( + + + + )} + ); const deleteButton = (