From 5b52ac992eb837d3642bd9a1f3aca31de074b8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:57:37 +0100 Subject: [PATCH] [DASHBOARDS] Tab duplication (#15906) Closes https://github.com/twentyhq/core-team-issues/issues/1878 - Allow tab duplication - Add autofocus on the tiltle input upon the creation and duplication of tabs and the creation of widgets - Fix a bug where cancelling the edition while on a new tab, by resetting the active tab id if it's not in the persisted tabs https://github.com/user-attachments/assets/a4dc2f0b-1a56-406a-bde7-cca17f9cdbc1 --- .../components/SidePanelHeader.tsx | 84 ++++++---- .../components/SidePanelHeaderSyncEffect.tsx | 17 ++ .../__stories__/SidePanelHeader.stories.tsx | 12 +- .../hooks/useNavigateCommandMenu.ts | 12 ++ .../CommandMenuPageLayoutTabSettings.tsx | 15 ++ .../CommandMenuPageLayoutWidgetTypeSelect.tsx | 2 + .../settings/TabSettingsSelectableItemIds.ts | 1 + .../hooks/useNavigatePageLayoutCommandMenu.ts | 3 + ...MenuShouldFocusTitleInputComponentState.ts | 10 ++ .../components/PageLayoutRendererContent.tsx | 1 + .../hooks/useDuplicatePageLayoutTab.ts | 156 ++++++++++++++++++ ...setDraftPageLayoutToPersistedPageLayout.ts | 26 +++ .../ui/input/components/TitleInput.tsx | 13 ++ .../components/TitleInputAutoOpenEffect.tsx | 52 ++++++ 14 files changed, 368 insertions(+), 36 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu/components/SidePanelHeaderSyncEffect.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/states/commandMenuShouldFocusTitleInputComponentState.ts create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutTab.ts create mode 100644 packages/twenty-front/src/modules/ui/input/components/TitleInputAutoOpenEffect.tsx diff --git a/packages/twenty-front/src/modules/command-menu/components/SidePanelHeader.tsx b/packages/twenty-front/src/modules/command-menu/components/SidePanelHeader.tsx index b8c347c285..ce7016d9ea 100644 --- a/packages/twenty-front/src/modules/command-menu/components/SidePanelHeader.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/SidePanelHeader.tsx @@ -1,5 +1,8 @@ +import { SidePanelHeaderTitleSyncEffect } from '@/command-menu/components/SidePanelHeaderSyncEffect'; import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo'; +import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState'; import { TitleInput } from '@/ui/input/components/TitleInput'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useState } from 'react'; @@ -73,6 +76,9 @@ export const SidePanelHeader = ({ onTitleChange, iconTooltip, }: SidePanelHeaderProps) => { + const [shouldFocusTitleInput, setShouldFocusTitleInput] = + useRecoilComponentState(commandMenuShouldFocusTitleInputComponentState); + const theme = useTheme(); const [title, setTitle] = useState(initialTitle); @@ -94,41 +100,49 @@ export const SidePanelHeader = ({ const tooltipId = `side-panel-icon-tooltip-${headerType.replace(/\s+/g, '-')}`; return ( - - - - - {iconTooltip && ( - - )} - - - { - setTitle(initialTitle); - }} - onClickOutside={saveTitle} - onTab={saveTitle} - onShiftTab={saveTitle} + <> + + + + - - {headerType} - - + + {iconTooltip && ( + + )} + + + { + setTitle(initialTitle); + }} + onClickOutside={saveTitle} + onTab={saveTitle} + onShiftTab={saveTitle} + shouldOpen={shouldFocusTitleInput} + onOpen={() => setShouldFocusTitleInput(false)} + /> + + {headerType} + + + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/components/SidePanelHeaderSyncEffect.tsx b/packages/twenty-front/src/modules/command-menu/components/SidePanelHeaderSyncEffect.tsx new file mode 100644 index 0000000000..0eb5df13ae --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/SidePanelHeaderSyncEffect.tsx @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; + +type SidePanelHeaderTitleSyncEffectProps = { + initialTitle: string; + setTitle: (title: string) => void; +}; + +export const SidePanelHeaderTitleSyncEffect = ({ + initialTitle, + setTitle, +}: SidePanelHeaderTitleSyncEffectProps) => { + useEffect(() => { + setTitle(initialTitle); + }, [initialTitle, setTitle]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/SidePanelHeader.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/SidePanelHeader.stories.tsx index 8a9f19385f..e1963a8c67 100644 --- a/packages/twenty-front/src/modules/command-menu/components/__stories__/SidePanelHeader.stories.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/SidePanelHeader.stories.tsx @@ -3,6 +3,7 @@ import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; import { IconPlus } from 'twenty-ui/display'; import { ComponentDecorator } from 'twenty-ui/testing'; import { THEME_LIGHT } from 'twenty-ui/theme'; +import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext'; import { SidePanelHeader } from '../SidePanelHeader'; const meta: Meta = { @@ -12,7 +13,16 @@ const meta: Meta = { onTitleChange: fn(), }, argTypes: {}, - decorators: [ComponentDecorator], + decorators: [ + ComponentDecorator, + (Story) => ( + + + + ), + ], parameters: { disableHotkeyInitialization: true, }, diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts index 76fd761c78..5946825fa0 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts @@ -6,6 +6,7 @@ import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/state import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState'; import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; @@ -88,9 +89,11 @@ export const useNavigateCommandMenu = () => { pageIcon, pageIconColor, pageId, + focusTitleInput = false, resetNavigationStack = false, }: CommandMenuNavigationStackItem & { resetNavigationStack?: boolean; + focusTitleInput?: boolean; }) => { const computedPageId = pageId || v4(); @@ -102,6 +105,15 @@ export const useNavigateCommandMenu = () => { instanceId: computedPageId, }); + if (focusTitleInput) { + set( + commandMenuShouldFocusTitleInputComponentState.atomFamily({ + instanceId: computedPageId, + }), + true, + ); + } + const isCommandMenuClosing = snapshot .getLoadable(isCommandMenuClosingState) .getValue(); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutTabSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutTabSettings.tsx index 59d028b2fe..fc45a8661d 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutTabSettings.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutTabSettings.tsx @@ -6,6 +6,7 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { TAB_SETTINGS_SELECTABLE_ITEM_IDS } from '@/command-menu/pages/page-layout/constants/settings/TabSettingsSelectableItemIds'; import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; import { useDeletePageLayoutTab } from '@/page-layout/hooks/useDeletePageLayoutTab'; +import { useDuplicatePageLayoutTab } from '@/page-layout/hooks/useDuplicatePageLayoutTab'; import { useMovePageLayoutTab } from '@/page-layout/hooks/useMovePageLayoutTab'; import { useUpdatePageLayoutTab } from '@/page-layout/hooks/useUpdatePageLayoutTab'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; @@ -22,6 +23,7 @@ import { IconAppWindow, IconChevronLeft, IconChevronRight, + IconCopyPlus, IconTrash, } from 'twenty-ui/display'; @@ -34,6 +36,7 @@ export const CommandMenuPageLayoutTabSettings = () => { pageLayoutDraftComponentState, pageLayoutId, ); + const [openTabId, setOpenTabId] = useRecoilComponentState( pageLayoutTabSettingsOpenTabIdComponentState, pageLayoutId, @@ -42,6 +45,7 @@ export const CommandMenuPageLayoutTabSettings = () => { const { moveLeft, moveRight } = useMovePageLayoutTab(pageLayoutId); const { deleteTab } = useDeletePageLayoutTab(pageLayoutId); const { updatePageLayoutTab } = useUpdatePageLayoutTab(pageLayoutId); + const { duplicateTab } = useDuplicatePageLayoutTab(pageLayoutId); if (!isDefined(openTabId)) { return null; @@ -105,6 +109,17 @@ export const CommandMenuPageLayoutTabSettings = () => { disabled={disableMoveRight} /> + duplicateTab(tab.id)} + > + duplicateTab(tab.id)} + /> + { navigatePageLayoutCommandMenu({ commandMenuPage: CommandMenuPages.PageLayoutGraphTypeSelect, + focusTitleInput: true, }); }; @@ -59,6 +60,7 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => { navigatePageLayoutCommandMenu({ commandMenuPage: CommandMenuPages.PageLayoutIframeSettings, + focusTitleInput: true, }); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/TabSettingsSelectableItemIds.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/TabSettingsSelectableItemIds.ts index 0479e4cbe9..fd8c55b67a 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/TabSettingsSelectableItemIds.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/TabSettingsSelectableItemIds.ts @@ -1,5 +1,6 @@ export const TAB_SETTINGS_SELECTABLE_ITEM_IDS = { MOVE_LEFT: 'tab-move-left', MOVE_RIGHT: 'tab-move-right', + DUPLICATE: 'tab-duplicate', DELETE: 'tab-delete', } as const; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu.ts index 684c1c1039..8706ec5571 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu.ts @@ -10,6 +10,7 @@ type NavigatePageLayoutCommandMenuProps = { commandMenuPage: PageLayoutCommandMenuPage; pageTitle?: string; pageIcon?: IconComponent; + focusTitleInput?: boolean; }; export const useNavigatePageLayoutCommandMenu = () => { @@ -20,6 +21,7 @@ export const useNavigatePageLayoutCommandMenu = () => { commandMenuPage, pageTitle, pageIcon, + focusTitleInput = false, }: NavigatePageLayoutCommandMenuProps) => { navigateCommandMenu({ page: commandMenuPage, @@ -29,6 +31,7 @@ export const useNavigatePageLayoutCommandMenu = () => { pageIcon: isDefined(pageIcon) ? pageIcon : getPageLayoutIcon(commandMenuPage), + focusTitleInput, }); }; }, [navigateCommandMenu]); diff --git a/packages/twenty-front/src/modules/command-menu/states/commandMenuShouldFocusTitleInputComponentState.ts b/packages/twenty-front/src/modules/command-menu/states/commandMenuShouldFocusTitleInputComponentState.ts new file mode 100644 index 0000000000..b880a166c1 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/states/commandMenuShouldFocusTitleInputComponentState.ts @@ -0,0 +1,10 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +import { CommandMenuPageComponentInstanceContext } from './contexts/CommandMenuPageComponentInstanceContext'; + +export const commandMenuShouldFocusTitleInputComponentState = + createComponentState({ + key: 'commandMenuShouldFocusTitleInputComponentState', + defaultValue: false, + componentInstanceContext: CommandMenuPageComponentInstanceContext, + }); 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 9088d7aa1b..25b7538167 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx @@ -74,6 +74,7 @@ export const PageLayoutRendererContent = () => { setTabSettingsOpenTabId(newTabId); navigatePageLayoutCommandMenu({ commandMenuPage: CommandMenuPages.PageLayoutTabSettings, + focusTitleInput: true, }); } : undefined; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutTab.ts b/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutTab.ts new file mode 100644 index 0000000000..45ae2ef860 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutTab.ts @@ -0,0 +1,156 @@ +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { calculateNewPosition } from '@/favorites/utils/calculateNewPosition'; +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId'; +import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition'; +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +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 { pageLayoutCurrentLayoutsComponentState } from '../states/pageLayoutCurrentLayoutsComponentState'; +import { pageLayoutDraftComponentState } from '../states/pageLayoutDraftComponentState'; +import { pageLayoutTabSettingsOpenTabIdComponentState } from '../states/pageLayoutTabSettingsOpenTabIdComponentState'; +import { type PageLayoutTab } from '../types/PageLayoutTab'; + +export const useDuplicatePageLayoutTab = (pageLayoutIdFromProps?: string) => { + const pageLayoutId = useAvailableComponentInstanceIdOrThrow( + PageLayoutComponentInstanceContext, + pageLayoutIdFromProps, + ); + + const pageLayoutDraftState = useRecoilComponentCallbackState( + pageLayoutDraftComponentState, + pageLayoutId, + ); + + const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState( + pageLayoutCurrentLayoutsComponentState, + pageLayoutId, + ); + + const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId); + const setActiveTabId = useSetRecoilComponentState( + activeTabIdComponentState, + tabListInstanceId, + ); + + const setTabSettingsOpenTabId = useSetRecoilComponentState( + pageLayoutTabSettingsOpenTabIdComponentState, + pageLayoutId, + ); + + const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu(); + + const { closeCommandMenu } = useCommandMenu(); + + const duplicateTab = useRecoilCallback( + ({ snapshot, set }) => + (tabId: string): string => { + const pageLayoutDraft = snapshot + .getLoadable(pageLayoutDraftState) + .getValue(); + + const allTabLayouts = snapshot + .getLoadable(pageLayoutCurrentLayoutsState) + .getValue(); + + const sourceTab = pageLayoutDraft.tabs.find((t) => t.id === tabId); + + if (!isDefined(sourceTab)) { + throw new Error(`Tab with id ${tabId} not found`); + } + + const newTabId = uuidv4(); + const widgetOldIdNewIdMap = new Map(); + + const clonedWidgets = sourceTab.widgets.map((widget) => { + const newWidgetId = uuidv4(); + widgetOldIdNewIdMap.set(widget.id, newWidgetId); + + return { + ...widget, + id: newWidgetId, + pageLayoutTabId: newTabId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + }); + + const sortedTabs = sortTabsByPosition(pageLayoutDraft.tabs); + const sourceIndex = sortedTabs.findIndex((t) => t.id === tabId); + + const newTabPosition = calculateNewPosition({ + items: sortedTabs, + destinationIndex: sourceIndex + 1, + sourceIndex, + }); + + const newTab: PageLayoutTab = { + ...sourceTab, + id: newTabId, + title: sourceTab.title.endsWith('(Copy)') + ? sourceTab.title + : `${sourceTab.title} (Copy)`, + position: newTabPosition, + widgets: clonedWidgets, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + const sourceLayouts = allTabLayouts[tabId] ?? { + desktop: [], + mobile: [], + }; + + const newLayouts = { + desktop: sourceLayouts.desktop.map((layout) => ({ + ...layout, + i: widgetOldIdNewIdMap.get(layout.i) || layout.i, + })), + mobile: sourceLayouts.mobile.map((layout) => ({ + ...layout, + i: widgetOldIdNewIdMap.get(layout.i) || layout.i, + })), + }; + + set(pageLayoutCurrentLayoutsState, { + ...allTabLayouts, + [newTabId]: newLayouts, + }); + + set(pageLayoutDraftState, (prev) => ({ + ...prev, + tabs: [...prev.tabs, newTab], + })); + + closeCommandMenu(); + + setActiveTabId(newTabId); + + setTabSettingsOpenTabId(newTabId); + + navigatePageLayoutCommandMenu({ + commandMenuPage: CommandMenuPages.PageLayoutTabSettings, + pageTitle: newTab.title, + focusTitleInput: true, + }); + + return newTabId; + }, + [ + closeCommandMenu, + navigatePageLayoutCommandMenu, + pageLayoutCurrentLayoutsState, + pageLayoutDraftState, + setActiveTabId, + setTabSettingsOpenTabId, + ], + ); + + return { duplicateTab }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts b/packages/twenty-front/src/modules/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts index 37a6c7c898..6126fe239a 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts @@ -3,6 +3,8 @@ import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pag import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts'; +import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId'; +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; @@ -16,6 +18,9 @@ export const useResetDraftPageLayoutToPersistedPageLayout = ( pageLayoutIdFromProps, ); + const tabListComponentInstanceId = + getTabListInstanceIdFromPageLayoutId(componentInstanceId); + const pageLayoutDraftState = useRecoilComponentCallbackState( pageLayoutDraftComponentState, componentInstanceId, @@ -31,6 +36,11 @@ export const useResetDraftPageLayoutToPersistedPageLayout = ( componentInstanceId, ); + const activeTabIdState = useRecoilComponentCallbackState( + activeTabIdComponentState, + tabListComponentInstanceId, + ); + const resetDraftPageLayoutToPersistedPageLayout = useRecoilCallback( ({ set, snapshot }) => () => { @@ -39,6 +49,21 @@ export const useResetDraftPageLayoutToPersistedPageLayout = ( .getValue(); if (isDefined(pageLayoutPersisted)) { + const currentActiveTabId = snapshot + .getLoadable(activeTabIdState) + .getValue(); + + const persistedTabIds = pageLayoutPersisted.tabs.map((tab) => tab.id); + const isActiveTabInPersistedTabs = + currentActiveTabId && persistedTabIds.includes(currentActiveTabId); + + if ( + !isActiveTabInPersistedTabs && + pageLayoutPersisted.tabs.length > 0 + ) { + set(activeTabIdState, pageLayoutPersisted.tabs[0].id); + } + set(pageLayoutDraftState, { id: pageLayoutPersisted.id, name: pageLayoutPersisted.name, @@ -55,6 +80,7 @@ export const useResetDraftPageLayoutToPersistedPageLayout = ( pageLayoutDraftState, pageLayoutPersistedState, pageLayoutCurrentLayoutsState, + activeTabIdState, ], ); diff --git a/packages/twenty-front/src/modules/ui/input/components/TitleInput.tsx b/packages/twenty-front/src/modules/ui/input/components/TitleInput.tsx index 8a1a3111b7..df532d7423 100644 --- a/packages/twenty-front/src/modules/ui/input/components/TitleInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/TitleInput.tsx @@ -3,6 +3,7 @@ import { useRef, useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { useRegisterInputEvents } from '@/object-record/record-field/ui/meta-types/input/hooks/useRegisterInputEvents'; +import { TitleInputAutoOpenEffect } from '@/ui/input/components/TitleInputAutoOpenEffect'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; @@ -24,6 +25,8 @@ type InputProps = { export type TitleInputProps = { disabled?: boolean; + shouldOpen?: boolean; + onOpen?: () => void; } & InputProps; const StyledDiv = styled.div<{ @@ -142,6 +145,8 @@ export const TitleInput = ({ onClickOutside, onTab, onShiftTab, + shouldOpen, + onOpen, }: TitleInputProps) => { const [isOpened, setIsOpened] = useState(false); @@ -149,6 +154,14 @@ export const TitleInput = ({ return ( <> + {isOpened ? ( void; + setIsOpened: (isOpened: boolean) => void; +}; + +export const TitleInputAutoOpenEffect = ({ + shouldOpen, + isOpened, + disabled, + instanceId, + onOpen, + setIsOpened, +}: TitleInputAutoOpenEffectProps) => { + const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack(); + + useEffect(() => { + if (isDefined(shouldOpen) && shouldOpen && !isOpened && !disabled) { + setIsOpened(true); + pushFocusItemToFocusStack({ + focusId: instanceId, + component: { + type: FocusComponentType.TEXT_INPUT, + instanceId: instanceId, + }, + globalHotkeysConfig: { + enableGlobalHotkeysConflictingWithKeyboard: false, + }, + }); + onOpen?.(); + } + }, [ + shouldOpen, + isOpened, + disabled, + instanceId, + pushFocusItemToFocusStack, + onOpen, + setIsOpened, + ]); + + return null; +};