From 8cb803cedf2d9661f9b3320aabe298c79134e711 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Wed, 15 Apr 2026 17:12:57 +0200 Subject: [PATCH] Various bug fixes Record page layouts (#19719) Fixes: - Can't add multiple widgets in a row - Ensure newly created is always focused --- .../hooks/useNavigateToMoreWidgets.ts | 35 ++++++++- .../hooks/usePageLayoutAddTabStrategy.ts | 13 ++++ ...widgetCreationTargetTabIdComponentState.ts | 11 +++ .../createDefaultFieldWidget.test.ts | 59 +++++++++++++++ .../createDefaultFieldsWidget.test.ts | 46 ++++++++++++ .../createDefaultFrontComponentWidget.test.ts | 52 +++++++++++++ .../__tests__/isReactivatableTab.test.ts | 51 +++++++++++++ .../components/RecordPageAddWidgetSection.tsx | 1 + ...elPageLayoutRecordPageWidgetTypeSelect.tsx | 51 +++---------- .../SidePanelPageLayoutTabSettingsContent.tsx | 8 +- ...resolveWidgetTypeSelectTargetTabId.test.ts | 73 +++++++++++++++++++ .../resolveWidgetTypeSelectTargetTabId.ts | 34 +++++++++ 12 files changed, 393 insertions(+), 41 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/states/widgetCreationTargetTabIdComponentState.ts create mode 100644 packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultFieldWidget.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultFieldsWidget.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultFrontComponentWidget.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/utils/__tests__/isReactivatableTab.test.ts create mode 100644 packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/__tests__/resolveWidgetTypeSelectTargetTabId.test.ts create mode 100644 packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/resolveWidgetTypeSelectTargetTabId.ts diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useNavigateToMoreWidgets.ts b/packages/twenty-front/src/modules/page-layout/hooks/useNavigateToMoreWidgets.ts index cf859fe4fe..c7efdb47dc 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useNavigateToMoreWidgets.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useNavigateToMoreWidgets.ts @@ -1,15 +1,48 @@ +import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext'; +import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { widgetCreationTargetTabIdComponentState } from '@/page-layout/states/widgetCreationTargetTabIdComponentState'; +import { widgetInsertionContextComponentState } from '@/page-layout/states/widgetInsertionContextComponentState'; import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel'; +import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { useStore } from 'jotai'; import { useCallback } from 'react'; import { SidePanelPages } from 'twenty-shared/types'; export const useNavigateToMoreWidgets = () => { + const { tabId } = usePageLayoutContentContext(); + const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel(); + const pageLayoutEditingWidgetIdState = useAtomComponentStateCallbackState( + pageLayoutEditingWidgetIdComponentState, + ); + + const widgetCreationTargetTabIdState = useAtomComponentStateCallbackState( + widgetCreationTargetTabIdComponentState, + ); + + const widgetInsertionContextState = useAtomComponentStateCallbackState( + widgetInsertionContextComponentState, + ); + + const store = useStore(); + const navigateToMoreWidgets = useCallback(() => { + store.set(pageLayoutEditingWidgetIdState, null); + store.set(widgetInsertionContextState, null); + store.set(widgetCreationTargetTabIdState, tabId); + navigatePageLayoutSidePanel({ sidePanelPage: SidePanelPages.PageLayoutRecordPageWidgetTypeSelect, }); - }, [navigatePageLayoutSidePanel]); + }, [ + navigatePageLayoutSidePanel, + pageLayoutEditingWidgetIdState, + store, + tabId, + widgetCreationTargetTabIdState, + widgetInsertionContextState, + ]); return { navigateToMoreWidgets }; }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutAddTabStrategy.ts b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutAddTabStrategy.ts index 7687e766d6..172f976bb9 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutAddTabStrategy.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutAddTabStrategy.ts @@ -7,10 +7,12 @@ import { type PageLayoutAddTabStrategy } from '@/page-layout/types/PageLayoutAdd import { isReactivatableTab } from '@/page-layout/utils/isReactivatableTab'; import { shouldEnableTabEditingFeatures } from '@/page-layout/utils/shouldEnableTabEditingFeatures'; import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel'; +import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { t } from '@lingui/core/macro'; import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; import { SidePanelPages } from 'twenty-shared/types'; import { FeatureFlagKey, PageLayoutType } from '~/generated-metadata/graphql'; @@ -40,8 +42,17 @@ export const usePageLayoutAddTabStrategy = ({ const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel(); + const { isInSidePanel } = useLayoutRenderingContext(); + + const navigate = useNavigate(); + const onCreate = useCallback(() => { const newTabId = createPageLayoutTab(t`Untitled`); + + if (!isInSidePanel) { + navigate(`#${newTabId}`); + } + setPageLayoutTabSettingsOpenTabId(newTabId); navigatePageLayoutSidePanel({ sidePanelPage: SidePanelPages.PageLayoutTabSettings, @@ -49,6 +60,8 @@ export const usePageLayoutAddTabStrategy = ({ }); }, [ createPageLayoutTab, + isInSidePanel, + navigate, setPageLayoutTabSettingsOpenTabId, navigatePageLayoutSidePanel, ]); diff --git a/packages/twenty-front/src/modules/page-layout/states/widgetCreationTargetTabIdComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/widgetCreationTargetTabIdComponentState.ts new file mode 100644 index 0000000000..de33938ba3 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/widgetCreationTargetTabIdComponentState.ts @@ -0,0 +1,11 @@ +import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const widgetCreationTargetTabIdComponentState = createAtomComponentState< + string | null +>({ + key: 'widgetCreationTargetTabIdComponentState', + defaultValue: null, + componentInstanceContext: PageLayoutComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultFieldWidget.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultFieldWidget.test.ts new file mode 100644 index 0000000000..517ed80211 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultFieldWidget.test.ts @@ -0,0 +1,59 @@ +import { createDefaultFieldWidget } from '@/page-layout/utils/createDefaultFieldWidget'; +import { + FieldDisplayMode, + PageLayoutTabLayoutMode, + WidgetConfigurationType, + WidgetType, +} from '~/generated-metadata/graphql'; + +describe('createDefaultFieldWidget', () => { + it('should return a FIELD widget with CARD display mode by default', () => { + const widget = createDefaultFieldWidget({ + id: 'widget-1', + pageLayoutTabId: 'tab-1', + title: 'Company Name', + fieldMetadataId: 'field-1', + objectMetadataId: 'object-1', + positionIndex: 0, + }); + + expect(widget).toMatchObject({ + __typename: 'PageLayoutWidget', + id: 'widget-1', + pageLayoutTabId: 'tab-1', + title: 'Company Name', + isActive: true, + type: WidgetType.FIELD, + configuration: { + __typename: 'FieldConfiguration', + configurationType: WidgetConfigurationType.FIELD, + fieldMetadataId: 'field-1', + fieldDisplayMode: FieldDisplayMode.CARD, + }, + position: { + __typename: 'PageLayoutWidgetVerticalListPosition', + layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, + index: 0, + }, + objectMetadataId: 'object-1', + deletedAt: null, + }); + }); + + it('should use the provided fieldDisplayMode', () => { + const widget = createDefaultFieldWidget({ + id: 'widget-2', + pageLayoutTabId: 'tab-1', + title: 'Description', + fieldMetadataId: 'field-2', + fieldDisplayMode: FieldDisplayMode.EDITOR, + objectMetadataId: 'object-1', + positionIndex: 1, + }); + + expect(widget.configuration).toMatchObject({ + fieldDisplayMode: FieldDisplayMode.EDITOR, + }); + expect(widget.position).toMatchObject({ index: 1 }); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultFieldsWidget.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultFieldsWidget.test.ts new file mode 100644 index 0000000000..31ddc4cc33 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultFieldsWidget.test.ts @@ -0,0 +1,46 @@ +import { createDefaultFieldsWidget } from '@/page-layout/utils/createDefaultFieldsWidget'; +import { + PageLayoutTabLayoutMode, + WidgetConfigurationType, + WidgetType, +} from '~/generated-metadata/graphql'; + +describe('createDefaultFieldsWidget', () => { + it('should return a FIELDS widget with correct properties', () => { + const widget = createDefaultFieldsWidget({ + id: 'widget-1', + pageLayoutTabId: 'tab-1', + viewId: 'view-1', + objectMetadataId: 'object-1', + positionIndex: 3, + }); + + expect(widget).toMatchObject({ + __typename: 'PageLayoutWidget', + id: 'widget-1', + pageLayoutTabId: 'tab-1', + title: 'Fields', + isActive: true, + type: WidgetType.FIELDS, + configuration: { + __typename: 'FieldsConfiguration', + configurationType: WidgetConfigurationType.FIELDS, + viewId: 'view-1', + }, + gridPosition: { + __typename: 'GridPosition', + row: 0, + column: 0, + rowSpan: 1, + columnSpan: 12, + }, + position: { + __typename: 'PageLayoutWidgetVerticalListPosition', + layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, + index: 3, + }, + objectMetadataId: 'object-1', + deletedAt: null, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultFrontComponentWidget.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultFrontComponentWidget.test.ts new file mode 100644 index 0000000000..055a451f50 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/createDefaultFrontComponentWidget.test.ts @@ -0,0 +1,52 @@ +import { createDefaultFrontComponentWidget } from '@/page-layout/utils/createDefaultFrontComponentWidget'; +import { + type GridPosition, + PageLayoutTabLayoutMode, + WidgetConfigurationType, + WidgetType, +} from '~/generated-metadata/graphql'; + +describe('createDefaultFrontComponentWidget', () => { + it('should return a FRONT_COMPONENT widget with grid position mapped to position', () => { + const gridPosition: GridPosition = { + __typename: 'GridPosition', + row: 2, + column: 3, + rowSpan: 4, + columnSpan: 6, + }; + + const widget = createDefaultFrontComponentWidget( + 'widget-1', + 'tab-1', + 'My Component', + 'front-comp-1', + gridPosition, + ); + + expect(widget).toMatchObject({ + __typename: 'PageLayoutWidget', + id: 'widget-1', + pageLayoutTabId: 'tab-1', + title: 'My Component', + isActive: true, + type: WidgetType.FRONT_COMPONENT, + configuration: { + __typename: 'FrontComponentConfiguration', + configurationType: WidgetConfigurationType.FRONT_COMPONENT, + frontComponentId: 'front-comp-1', + }, + gridPosition, + position: { + __typename: 'PageLayoutWidgetGridPosition', + layoutMode: PageLayoutTabLayoutMode.GRID, + row: 2, + column: 3, + rowSpan: 4, + columnSpan: 6, + }, + objectMetadataId: null, + deletedAt: null, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/isReactivatableTab.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/isReactivatableTab.test.ts new file mode 100644 index 0000000000..0272442e64 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/isReactivatableTab.test.ts @@ -0,0 +1,51 @@ +import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; +import { isReactivatableTab } from '@/page-layout/utils/isReactivatableTab'; + +const makeTab = (overrides: Partial = {}): PageLayoutTab => + ({ + id: 'tab-1', + applicationId: 'app-1', + title: 'Tab', + isActive: true, + position: 0, + pageLayoutId: '', + widgets: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + deletedAt: null, + ...overrides, + }) as unknown as PageLayoutTab; + +describe('isReactivatableTab', () => { + it('should return true when tab is inactive and applicationId matches', () => { + const tab = makeTab({ isActive: false, applicationId: 'app-1' }); + + expect(isReactivatableTab({ tab, objectApplicationId: 'app-1' })).toBe( + true, + ); + }); + + it('should return false when tab is active', () => { + const tab = makeTab({ isActive: true, applicationId: 'app-1' }); + + expect(isReactivatableTab({ tab, objectApplicationId: 'app-1' })).toBe( + false, + ); + }); + + it('should return false when applicationId does not match', () => { + const tab = makeTab({ isActive: false, applicationId: 'app-1' }); + + expect(isReactivatableTab({ tab, objectApplicationId: 'app-2' })).toBe( + false, + ); + }); + + it('should return false when objectApplicationId is undefined', () => { + const tab = makeTab({ isActive: false, applicationId: 'app-1' }); + + expect(isReactivatableTab({ tab, objectApplicationId: undefined })).toBe( + false, + ); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/RecordPageAddWidgetSection.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/RecordPageAddWidgetSection.tsx index b4e8a8e50f..2665f45fd1 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/RecordPageAddWidgetSection.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/RecordPageAddWidgetSection.tsx @@ -36,6 +36,7 @@ const StyledHeader = styled.div` `; const StyledMenuItemList = styled.div` + background-color: ${themeCssVariables.background.secondary}; border: 1px solid ${themeCssVariables.border.color.medium}; border-radius: ${themeCssVariables.border.radius.md}; margin-top: ${themeCssVariables.spacing[2]}; diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect.tsx index 303b4c4612..1821db3c50 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect.tsx @@ -4,12 +4,12 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useInsertCreatedWidgetAtContext } from '@/page-layout/hooks/useInsertCreatedWidgetAtContext'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { widgetCreationTargetTabIdComponentState } from '@/page-layout/states/widgetCreationTargetTabIdComponentState'; import { widgetInsertionContextComponentState } from '@/page-layout/states/widgetInsertionContextComponentState'; import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; import { createDefaultFieldWidget } from '@/page-layout/utils/createDefaultFieldWidget'; import { createDefaultFieldsWidget } from '@/page-layout/utils/createDefaultFieldsWidget'; -import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord'; import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPosition'; import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab'; import { useFieldWidgetEligibleFields } from '@/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields'; @@ -20,8 +20,8 @@ import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel'; import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore'; import { getFrontComponentWidgetTypeSelectItemId } from '@/side-panel/pages/page-layout/utils/getFrontComponentWidgetTypeSelectItemId'; +import { resolveWidgetTypeSelectTargetTabId } from '@/side-panel/pages/page-layout/utils/resolveWidgetTypeSelectTargetTabId'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; -import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState'; import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; @@ -41,11 +41,8 @@ import { } from '~/generated-metadata/graphql'; export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { - const { - pageLayoutId, - recordId, - objectNameSingular: targetObjectNameSingular, - } = usePageLayoutIdFromContextStore(); + const { pageLayoutId, objectNameSingular: targetObjectNameSingular } = + usePageLayoutIdFromContextStore(); const { closeSidePanelMenu } = useSidePanelMenu(); @@ -72,15 +69,9 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { pageLayoutId, ); - const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({ + const widgetCreationTargetTabId = useAtomComponentStateValue( + widgetCreationTargetTabIdComponentState, pageLayoutId, - layoutType: pageLayoutDraft.type, - targetRecordIdentifier: { id: recordId, targetObjectNameSingular: '' }, - }); - - const activeTabId = useAtomComponentStateValue( - activeTabIdComponentState, - tabListInstanceId, ); const store = useStore(); @@ -96,13 +87,11 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { targetObjectNameSingular, ); - const editingWidgetTab = isDefined(pageLayoutEditingWidgetId) - ? pageLayoutDraft.tabs.find((tab) => - tab.widgets.some((widget) => widget.id === pageLayoutEditingWidgetId), - ) - : undefined; - - const tabId = editingWidgetTab?.id ?? activeTabId; + const tabId = resolveWidgetTypeSelectTargetTabId({ + pageLayoutEditingWidgetId, + tabs: pageLayoutDraft.tabs, + widgetCreationTargetTabId, + }); const isReplaceMode = isDefined(pageLayoutEditingWidgetId) && !isDefined(widgetInsertionContext); @@ -126,11 +115,7 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { }, [existingWidget]); const removeExistingWidgetIfReplacing = useCallback(() => { - if ( - !isReplaceMode || - !isDefined(pageLayoutEditingWidgetId) || - !isDefined(tabId) - ) { + if (!isReplaceMode || !isDefined(pageLayoutEditingWidgetId)) { return; } @@ -160,10 +145,6 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { ); const handleCreateFieldsWidget = useCallback(() => { - if (!isDefined(tabId)) { - return; - } - const replacePositionIndex = getExistingWidgetPositionIndex(); const viewId = uuidv4(); @@ -210,10 +191,6 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { ]); const handleCreateFieldWidget = useCallback(() => { - if (!isDefined(tabId)) { - return; - } - const replacePositionIndex = getExistingWidgetPositionIndex(); removeExistingWidgetIfReplacing(); @@ -287,10 +264,6 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { const handleCreateFrontComponentWidget = useCallback( (frontComponent: FrontComponent) => { - if (!isDefined(tabId)) { - return; - } - const replacePositionIndex = getExistingWidgetPositionIndex(); removeExistingWidgetIfReplacing(); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutTabSettingsContent.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutTabSettingsContent.tsx index fe44806134..0ad4c81c6a 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutTabSettingsContent.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutTabSettingsContent.tsx @@ -14,6 +14,7 @@ import { RegularTabSettingsContent } from '@/side-panel/pages/page-layout/compon import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useNavigate } from 'react-router-dom'; import { isDefined } from 'twenty-shared/utils'; import { PageLayoutTabLayoutMode, @@ -31,6 +32,8 @@ export const SidePanelPageLayoutTabSettingsContent = ({ }: SidePanelPageLayoutTabSettingsContentProps) => { const { closeSidePanelMenu } = useSidePanelMenu(); + const navigate = useNavigate(); + const currentWorkspace = useAtomStateValue(currentWorkspaceState); const pageLayoutDraft = useAtomComponentStateValue( @@ -129,7 +132,10 @@ export const SidePanelPageLayoutTabSettingsContent = ({ onMoveLeft={() => moveLeft(tab.id)} onMoveRight={() => moveRight(tab.id)} onSetAsPinned={() => setAsPinnedTab(tab.id)} - onDuplicate={() => duplicateTab(tab.id)} + onDuplicate={() => { + const newTabId = duplicateTab(tab.id); + navigate(`#${newTabId}`); + }} onResetToDefault={handleResetToDefault} onDelete={handleDelete} /> diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/__tests__/resolveWidgetTypeSelectTargetTabId.test.ts b/packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/__tests__/resolveWidgetTypeSelectTargetTabId.test.ts new file mode 100644 index 0000000000..4e970db6cd --- /dev/null +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/__tests__/resolveWidgetTypeSelectTargetTabId.test.ts @@ -0,0 +1,73 @@ +import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; +import { resolveWidgetTypeSelectTargetTabId } from '@/side-panel/pages/page-layout/utils/resolveWidgetTypeSelectTargetTabId'; +import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql'; + +const makeTab = (id: string, widgetIds: string[] = []): PageLayoutTab => + ({ + id, + applicationId: '', + title: id, + isActive: true, + position: 0, + layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, + pageLayoutId: '', + widgets: widgetIds.map((wId) => ({ id: wId })), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + deletedAt: null, + }) as unknown as PageLayoutTab; + +describe('resolveWidgetTypeSelectTargetTabId', () => { + it('should return the tab containing the editing widget', () => { + const tabs = [ + makeTab('tab-1', ['widget-a']), + makeTab('tab-2', ['widget-b']), + ]; + + const result = resolveWidgetTypeSelectTargetTabId({ + pageLayoutEditingWidgetId: 'widget-b', + tabs, + widgetCreationTargetTabId: null, + }); + + expect(result).toBe('tab-2'); + }); + + it('should throw when the editing widget is not found in any tab', () => { + const tabs = [makeTab('tab-1', ['widget-a'])]; + + expect(() => + resolveWidgetTypeSelectTargetTabId({ + pageLayoutEditingWidgetId: 'non-existent', + tabs, + widgetCreationTargetTabId: null, + }), + ).toThrow('Cannot find tab containing editing widget non-existent'); + }); + + it('should return widgetCreationTargetTabId when no editing widget is set', () => { + const tabs = [makeTab('tab-1', ['widget-a'])]; + + const result = resolveWidgetTypeSelectTargetTabId({ + pageLayoutEditingWidgetId: null, + tabs, + widgetCreationTargetTabId: 'tab-1', + }); + + expect(result).toBe('tab-1'); + }); + + it('should throw when both pageLayoutEditingWidgetId and widgetCreationTargetTabId are null', () => { + const tabs = [makeTab('tab-1')]; + + expect(() => + resolveWidgetTypeSelectTargetTabId({ + pageLayoutEditingWidgetId: null, + tabs, + widgetCreationTargetTabId: null, + }), + ).toThrow( + 'widgetCreationTargetTabId must be set when navigating to widget type select without an editing widget', + ); + }); +}); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/resolveWidgetTypeSelectTargetTabId.ts b/packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/resolveWidgetTypeSelectTargetTabId.ts new file mode 100644 index 0000000000..9ef6cf5a89 --- /dev/null +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/utils/resolveWidgetTypeSelectTargetTabId.ts @@ -0,0 +1,34 @@ +import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; +import { isDefined } from 'twenty-shared/utils'; + +export const resolveWidgetTypeSelectTargetTabId = ({ + pageLayoutEditingWidgetId, + tabs, + widgetCreationTargetTabId, +}: { + pageLayoutEditingWidgetId: string | null; + tabs: PageLayoutTab[]; + widgetCreationTargetTabId: string | null; +}): string => { + if (isDefined(pageLayoutEditingWidgetId)) { + const editingWidgetTab = tabs.find((tab) => + tab.widgets.some((widget) => widget.id === pageLayoutEditingWidgetId), + ); + + if (!isDefined(editingWidgetTab)) { + throw new Error( + `Cannot find tab containing editing widget ${pageLayoutEditingWidgetId}`, + ); + } + + return editingWidgetTab.id; + } + + if (!isDefined(widgetCreationTargetTabId)) { + throw new Error( + 'widgetCreationTargetTabId must be set when navigating to widget type select without an editing widget', + ); + } + + return widgetCreationTargetTabId; +};