From c805a351ab3cb250888e16fcda5751deb8295073 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Wed, 15 Apr 2026 11:11:55 +0200 Subject: [PATCH] Reactivate disabled full tab widgets (#19702) https://github.com/user-attachments/assets/14a48c7d-e731-4a15-883f-c53beb4943de --- .../PageLayoutSingleTabRenderer.tsx | 6 +- .../components/PageLayoutTabList.tsx | 95 ++++++++++--- ...PageLayoutTabListNewTabDropdownContent.tsx | 125 ++++++++++++++++++ .../components/PageLayoutTabsRenderer.tsx | 32 +---- .../__stories__/PageLayoutTabList.stories.tsx | 10 +- .../hooks/useDeletePageLayoutTab.ts | 33 ++--- .../hooks/usePageLayoutAddTabStrategy.ts | 77 +++++++++++ .../useRecordPageLayoutObjectApplicationId.ts | 25 ++++ .../hooks/useReorderPageLayoutTabs.ts | 5 +- .../pageLayoutsWithRelationsSelector.ts | 3 +- .../types/PageLayoutAddTabStrategy.ts | 4 + ...onvertPageLayoutDraftToUpdateInput.test.ts | 1 + .../convertPageLayoutDraftToUpdateInput.ts | 66 ++++----- .../page-layout/utils/isReactivatableTab.ts | 11 ++ 14 files changed, 383 insertions(+), 110 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListNewTabDropdownContent.tsx create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutAddTabStrategy.ts create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutObjectApplicationId.ts create mode 100644 packages/twenty-front/src/modules/page-layout/types/PageLayoutAddTabStrategy.ts create mode 100644 packages/twenty-front/src/modules/page-layout/utils/isReactivatableTab.ts diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutSingleTabRenderer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutSingleTabRenderer.tsx index 412a94d31c..a258a44974 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutSingleTabRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutSingleTabRenderer.tsx @@ -40,8 +40,10 @@ const PageLayoutSingleTabRendererInner = () => { const targetRecordIdentifier = useTargetRecord(); const { isInSidePanel } = useLayoutRenderingContext(); - const sortedTabs = sortTabsByPosition(currentPageLayout.tabs); - const firstTab = sortedTabs[0]; + const sortedActiveTabs = sortTabsByPosition( + currentPageLayout.tabs.filter((tab) => tab.isActive), + ); + const firstTab = sortedActiveTabs[0]; const firstTabWithVisibleWidgets = usePageLayoutTabWithVisibleWidgetsOrThrow( firstTab.id, diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabList.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabList.tsx index 5e76a68011..a94f5eb935 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabList.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabList.tsx @@ -11,7 +11,7 @@ import { useLingui } from '@lingui/react/macro'; import { useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { IconPlus, useIcons } from 'twenty-ui/display'; -import { IconButton } from 'twenty-ui/input'; +import { TabButton } from 'twenty-ui/input'; import { isPageLayoutTabDraggingComponentState } from '@/page-layout/states/isPageLayoutTabDraggingComponentState'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; @@ -29,6 +29,7 @@ import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomC import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; import { PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS } from '@/page-layout/components/PageLayoutTabListDroppableIds'; +import { PageLayoutTabListNewTabDropdownContent } from '@/page-layout/components/PageLayoutTabListNewTabDropdownContent'; import { PageLayoutTabListReorderableOverflowDropdown } from '@/page-layout/components/PageLayoutTabListReorderableOverflowDropdown'; import { PageLayoutTabListVisibleTabs } from '@/page-layout/components/PageLayoutTabListVisibleTabs'; import { STANDARD_PAGE_LAYOUT_TAB_TITLE_TRANSLATIONS } from '@/page-layout/constants/StandardPageLayoutTabTitleTranslations'; @@ -37,9 +38,11 @@ import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutIn import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { pageLayoutTabListCurrentDragDroppableIdComponentState } from '@/page-layout/states/pageLayoutTabListCurrentDragDroppableIdComponentState'; import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState'; +import { type PageLayoutAddTabStrategy } from '@/page-layout/types/PageLayoutAddTabStrategy'; import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; import { shouldEnableTabEditingFeatures } from '@/page-layout/utils/shouldEnableTabEditingFeatures'; import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { TabListDropdown } from '@/ui/layout/tab-list/components/TabListDropdown'; import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab-list/components/TabListFromUrlOptionalEffect'; import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps'; @@ -89,7 +92,7 @@ const StyledAddButton = styled.div` type PageLayoutTabListProps = Omit & { tabs: PageLayoutTab[]; isReorderEnabled: boolean; - onAddTab?: () => void; + addTabStrategy?: PageLayoutAddTabStrategy; onReorder?: (result: DropResult, provided: ResponderProvided) => boolean; behaveAsLinks: boolean; pageLayoutType: PageLayoutType; @@ -103,7 +106,7 @@ export const PageLayoutTabList = ({ className, componentInstanceId, onChangeTab, - onAddTab, + addTabStrategy, isReorderEnabled, onReorder, pageLayoutType, @@ -147,7 +150,7 @@ export const PageLayoutTabList = ({ onAddButtonWidthChange, } = useTabListMeasurements({ visibleTabs: tabsWithIcons, - hasAddButton: isDefined(onAddTab), + hasAddButton: isDefined(addTabStrategy), }); const pageLayoutId = useAvailableComponentInstanceIdOrThrow( @@ -155,6 +158,7 @@ export const PageLayoutTabList = ({ ); const dropdownId = `tab-overflow-${componentInstanceId}`; + const addTabDropdownId = `tab-add-${componentInstanceId}`; const { closeDropdown } = useCloseDropdown(); const { openDropdown } = useOpenDropdown(); const { toggleClickOutside } = useClickOutsideListener(dropdownId); @@ -358,11 +362,18 @@ export const PageLayoutTabList = ({ loading={loading} onTabWidthChange={onTabWidthChange} onMoreButtonWidthChange={onMoreButtonWidthChange} - onAddButtonWidthChange={onAddTab ? onAddButtonWidthChange : undefined} + onAddButtonWidthChange={ + addTabStrategy ? onAddButtonWidthChange : undefined + } addButtonMeasurement={ - onAddTab ? ( + addTabStrategy ? ( - + ) : undefined } @@ -405,13 +416,36 @@ export const PageLayoutTabList = ({ )} - {onAddTab && ( + {addTabStrategy?.mode === 'direct' && ( - onAddTab()} + addTabStrategy.onCreate()} + disableTestId + /> + + )} + {addTabStrategy?.mode === 'dropdown' && ( + + + } + dropdownComponents={ + + } + dropdownPlacement="bottom-start" /> )} @@ -445,13 +479,36 @@ export const PageLayoutTabList = ({ /> )} - {onAddTab && ( + {addTabStrategy?.mode === 'direct' && ( - onAddTab()} + addTabStrategy.onCreate()} + disableTestId + /> + + )} + {addTabStrategy?.mode === 'dropdown' && ( + + + } + dropdownComponents={ + + } + dropdownPlacement="bottom-start" /> )} diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListNewTabDropdownContent.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListNewTabDropdownContent.tsx new file mode 100644 index 0000000000..9ba1c871f6 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListNewTabDropdownContent.tsx @@ -0,0 +1,125 @@ +import { STANDARD_PAGE_LAYOUT_TAB_TITLE_TRANSLATIONS } from '@/page-layout/constants/StandardPageLayoutTabTitleTranslations'; +import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow'; +import { useIsCurrentObjectCustom } from '@/page-layout/hooks/useIsCurrentObjectCustom'; +import { useRecordPageLayoutObjectApplicationId } from '@/page-layout/hooks/useRecordPageLayoutObjectApplicationId'; +import { useUpdatePageLayoutTab } from '@/page-layout/hooks/useUpdatePageLayoutTab'; +import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState'; +import { isReactivatableTab } from '@/page-layout/utils/isReactivatableTab'; +import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition'; +import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel'; +import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; +import { useLingui } from '@lingui/react/macro'; +import { useCallback, useMemo } from 'react'; +import { SidePanelPages } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { IconPlus, useIcons } from 'twenty-ui/display'; +import { MenuItem } from 'twenty-ui/navigation'; + +type PageLayoutTabListNewTabDropdownContentProps = { + onCreate: () => void; + dropdownId: string; +}; + +export const PageLayoutTabListNewTabDropdownContent = ({ + onCreate, + dropdownId, +}: PageLayoutTabListNewTabDropdownContentProps) => { + const { t } = useLingui(); + const { getIcon } = useIcons(); + const { closeDropdown } = useCloseDropdown(); + const { isCustom } = useIsCurrentObjectCustom(); + + const shouldTranslateTabTitles = !isCustom; + + const { currentPageLayout } = useCurrentPageLayoutOrThrow(); + const { objectApplicationId } = useRecordPageLayoutObjectApplicationId(); + const { updatePageLayoutTab } = useUpdatePageLayoutTab(); + + const setActiveTabId = useSetAtomComponentState(activeTabIdComponentState); + const setPageLayoutTabSettingsOpenTabId = useSetAtomComponentState( + pageLayoutTabSettingsOpenTabIdComponentState, + ); + const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel(); + + const inactiveTabs = useMemo( + () => + sortTabsByPosition( + currentPageLayout.tabs.filter((tab) => + isReactivatableTab({ tab, objectApplicationId }), + ), + ), + [currentPageLayout.tabs, objectApplicationId], + ); + + const handleCreateEmptyTab = useCallback(() => { + onCreate(); + closeDropdown(dropdownId); + }, [onCreate, closeDropdown, dropdownId]); + + const handleReactivateTab = useCallback( + (tabId: string) => { + updatePageLayoutTab(tabId, { isActive: true }); + setActiveTabId(tabId); + setPageLayoutTabSettingsOpenTabId(tabId); + navigatePageLayoutSidePanel({ + sidePanelPage: SidePanelPages.PageLayoutTabSettings, + resetNavigationStack: true, + }); + closeDropdown(dropdownId); + }, + [ + updatePageLayoutTab, + setActiveTabId, + setPageLayoutTabSettingsOpenTabId, + navigatePageLayoutSidePanel, + closeDropdown, + dropdownId, + ], + ); + + const getTabTitle = (title: string) => { + if ( + shouldTranslateTabTitles && + isDefined(STANDARD_PAGE_LAYOUT_TAB_TITLE_TRANSLATIONS[title]) + ) { + return t(STANDARD_PAGE_LAYOUT_TAB_TITLE_TRANSLATIONS[title]); + } + return title; + }; + + return ( + + {t`New tab`} + + + + {inactiveTabs.length > 0 && ( + <> + + + + {inactiveTabs.map((tab) => ( + handleReactivateTab(tab.id)} + /> + ))} + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabsRenderer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabsRenderer.tsx index 31b205a879..3a851d3133 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabsRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabsRenderer.tsx @@ -4,29 +4,24 @@ import { PageLayoutLeftPanel } from '@/page-layout/components/PageLayoutLeftPane import { PageLayoutTabList } from '@/page-layout/components/PageLayoutTabList'; import { PageLayoutTabListEffect } from '@/page-layout/components/PageLayoutTabListEffect'; import { PAGE_LAYOUT_LEFT_PANEL_CONTAINER_WIDTH } from '@/page-layout/constants/PageLayoutLeftPanelContainerWidth'; -import { useCreatePageLayoutTab } from '@/page-layout/hooks/useCreatePageLayoutTab'; import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow'; import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode'; +import { usePageLayoutAddTabStrategy } from '@/page-layout/hooks/usePageLayoutAddTabStrategy'; import { useReorderRecordPageLayoutTabs } from '@/page-layout/hooks/useReorderRecordPageLayoutTabs'; import { PageLayoutMainContent } from '@/page-layout/PageLayoutMainContent'; -import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState'; import { getScrollWrapperInstanceIdFromPageLayoutId } from '@/page-layout/utils/getScrollWrapperInstanceIdFromPageLayoutId'; import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord'; import { getTabsByDisplayMode } from '@/page-layout/utils/getTabsByDisplayMode'; import { getTabsWithVisibleWidgets } from '@/page-layout/utils/getTabsWithVisibleWidgets'; import { shouldEnableTabEditingFeatures } from '@/page-layout/utils/shouldEnableTabEditingFeatures'; import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition'; -import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue'; -import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { styled } from '@linaria/react'; -import { t } from '@lingui/core/macro'; -import { SidePanelPages } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { useIsMobile } from 'twenty-ui/utilities'; import { FeatureFlagKey } from '~/generated-metadata/graphql'; @@ -67,17 +62,14 @@ export const PageLayoutTabsRenderer = () => { targetRecordIdentifier, }); - const { createPageLayoutTab } = useCreatePageLayoutTab({ + const addTabStrategy = usePageLayoutAddTabStrategy({ pageLayoutId: currentPageLayout.id, tabListInstanceId, }); + const { reorderRecordPageTabs } = useReorderRecordPageLayoutTabs( currentPageLayout.id, ); - const setPageLayoutTabSettingsOpenTabId = useSetAtomComponentState( - pageLayoutTabSettingsOpenTabIdComponentState, - ); - const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel(); const isMobile = useIsMobile(); @@ -96,22 +88,6 @@ export const PageLayoutTabsRenderer = () => { item.nameSingular === targetRecordIdentifier?.targetObjectNameSingular, )?.isSystem ?? false; - const handleAddTab = - isPageLayoutInEditMode && - shouldEnableTabEditingFeatures( - currentPageLayout.type, - isRecordPageGlobalEditionEnabled, - ) - ? () => { - const newTabId = createPageLayoutTab(t`Untitled`); - setPageLayoutTabSettingsOpenTabId(newTabId); - navigatePageLayoutSidePanel({ - sidePanelPage: SidePanelPages.PageLayoutTabSettings, - focusTitleInput: true, - }); - } - : undefined; - const canEnableTabEditing = isPageLayoutInEditMode && shouldEnableTabEditingFeatures( @@ -168,7 +144,7 @@ export const PageLayoutTabsRenderer = () => { behaveAsLinks={!isInSidePanel && !isPageLayoutInEditMode} isInSidePanel={isInSidePanel} componentInstanceId={tabListInstanceId} - onAddTab={handleAddTab} + addTabStrategy={addTabStrategy} isReorderEnabled={canEnableTabEditing} onReorder={ canEnableTabEditing diff --git a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutTabList.stories.tsx b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutTabList.stories.tsx index 6b25701289..245bf2d65f 100644 --- a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutTabList.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutTabList.stories.tsx @@ -1,5 +1,5 @@ -import { styled } from '@linaria/react'; import { type DropResult, type ResponderProvided } from '@hello-pangea/dnd'; +import { styled } from '@linaria/react'; import type { Meta, StoryObj } from '@storybook/react-vite'; import { useMemo, useState } from 'react'; import { ComponentWithRouterDecorator } from 'twenty-ui/testing'; @@ -11,8 +11,8 @@ import { PageLayoutEditModeProviderContext } from '@/page-layout/contexts/PageLa import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition'; -import { PageLayoutType } from '~/generated-metadata/graphql'; import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { PageLayoutType } from '~/generated-metadata/graphql'; const StyledContainer = styled.div` border: 1px solid ${themeCssVariables.border.color.strong}; @@ -170,7 +170,11 @@ const PageLayoutTabListPlayground = ({ componentInstanceId="page-layout-tab-list-story" behaveAsLinks={false} loading={false} - onAddTab={isReorderEnabled ? handleAddTab : undefined} + addTabStrategy={ + isReorderEnabled + ? { mode: 'direct', onCreate: handleAddTab } + : undefined + } isReorderEnabled={isReorderEnabled} onReorder={isReorderEnabled ? handleReorder : undefined} pageLayoutType={PageLayoutType.DASHBOARD} diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutTab.ts b/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutTab.ts index 1146b22fcf..5f4e016a23 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutTab.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutTab.ts @@ -1,7 +1,5 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; -import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; -import { removeTabLayouts } from '@/page-layout/utils/removeTabLayouts'; 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'; @@ -26,11 +24,6 @@ export const useDeletePageLayoutTab = ({ pageLayoutId, ); - const pageLayoutCurrentLayoutsState = useAtomComponentStateCallbackState( - pageLayoutCurrentLayoutsComponentState, - pageLayoutId, - ); - const store = useStore(); const activeTabIdAtom = activeTabIdComponentState.atomFamily({ @@ -40,36 +33,32 @@ export const useDeletePageLayoutTab = ({ const deleteTab = useCallback( (tabId: string) => { const draft = store.get(pageLayoutDraftState); - if (draft.tabs.length <= 1) { + const activeTabs = draft.tabs.filter((t) => t.isActive); + + if (activeTabs.length <= 1) { return; } - const sorted = sortTabsByPosition(draft.tabs); - const index = sorted.findIndex((t) => t.id === tabId); + const sortedActiveTabs = sortTabsByPosition(activeTabs); + const index = sortedActiveTabs.findIndex((t) => t.id === tabId); const activeTabId = store.get(activeTabIdAtom); - const allLayouts = store.get(pageLayoutCurrentLayoutsState); - const updatedLayouts = removeTabLayouts(allLayouts, tabId); - store.set(pageLayoutCurrentLayoutsState, updatedLayouts); - store.set(pageLayoutDraftState, (prev) => ({ ...prev, - tabs: prev.tabs.filter((t) => t.id !== tabId), + tabs: prev.tabs.map((t) => + t.id === tabId ? { ...t, isActive: false } : t, + ), })); if (activeTabId === tabId) { - const neighbor = index > 0 ? sorted[index - 1] : sorted[index + 1]; + const neighbor = + index > 0 ? sortedActiveTabs[index - 1] : sortedActiveTabs[index + 1]; const nextActiveId = neighbor?.id ?? null; store.set(activeTabIdAtom, nextActiveId); } }, - [ - pageLayoutCurrentLayoutsState, - pageLayoutDraftState, - activeTabIdAtom, - store, - ], + [pageLayoutDraftState, activeTabIdAtom, store], ); return { deleteTab }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutAddTabStrategy.ts b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutAddTabStrategy.ts new file mode 100644 index 0000000000..7687e766d6 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutAddTabStrategy.ts @@ -0,0 +1,77 @@ +import { useCreatePageLayoutTab } from '@/page-layout/hooks/useCreatePageLayoutTab'; +import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow'; +import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode'; +import { useRecordPageLayoutObjectApplicationId } from '@/page-layout/hooks/useRecordPageLayoutObjectApplicationId'; +import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState'; +import { type PageLayoutAddTabStrategy } from '@/page-layout/types/PageLayoutAddTabStrategy'; +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 { 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 { SidePanelPages } from 'twenty-shared/types'; +import { FeatureFlagKey, PageLayoutType } from '~/generated-metadata/graphql'; + +export const usePageLayoutAddTabStrategy = ({ + pageLayoutId, + tabListInstanceId, +}: { + pageLayoutId: string; + tabListInstanceId: string; +}): PageLayoutAddTabStrategy | undefined => { + const { currentPageLayout } = useCurrentPageLayoutOrThrow(); + const isPageLayoutInEditMode = useIsPageLayoutInEditMode(); + const { objectApplicationId } = useRecordPageLayoutObjectApplicationId(); + + const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED, + ); + + const { createPageLayoutTab } = useCreatePageLayoutTab({ + pageLayoutId, + tabListInstanceId, + }); + + const setPageLayoutTabSettingsOpenTabId = useSetAtomComponentState( + pageLayoutTabSettingsOpenTabIdComponentState, + ); + + const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel(); + + const onCreate = useCallback(() => { + const newTabId = createPageLayoutTab(t`Untitled`); + setPageLayoutTabSettingsOpenTabId(newTabId); + navigatePageLayoutSidePanel({ + sidePanelPage: SidePanelPages.PageLayoutTabSettings, + focusTitleInput: true, + }); + }, [ + createPageLayoutTab, + setPageLayoutTabSettingsOpenTabId, + navigatePageLayoutSidePanel, + ]); + + const isEnabled = + isPageLayoutInEditMode && + shouldEnableTabEditingFeatures( + currentPageLayout.type, + isRecordPageGlobalEditionEnabled, + ); + + if (!isEnabled) { + return undefined; + } + + const hasInactiveTabs = currentPageLayout.tabs.some((tab) => + isReactivatableTab({ tab, objectApplicationId }), + ); + + const mode = + currentPageLayout.type === PageLayoutType.RECORD_PAGE && hasInactiveTabs + ? 'dropdown' + : 'direct'; + + return { mode, onCreate }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutObjectApplicationId.ts b/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutObjectApplicationId.ts new file mode 100644 index 0000000000..ad03e7d059 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutObjectApplicationId.ts @@ -0,0 +1,25 @@ +import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; +import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { isDefined } from 'twenty-shared/utils'; + +export const useRecordPageLayoutObjectApplicationId = (): { + objectApplicationId: string | undefined; +} => { + const { currentPageLayout } = useCurrentPageLayoutOrThrow(); + const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); + + const objectMetadataId = currentPageLayout.objectMetadataId; + + if (!isDefined(objectMetadataId)) { + return { objectApplicationId: undefined }; + } + + const objectMetadataItem = objectMetadataItems.find( + (item) => item.id === objectMetadataId, + ); + + return { + objectApplicationId: objectMetadataItem?.applicationId, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useReorderPageLayoutTabs.ts b/packages/twenty-front/src/modules/page-layout/hooks/useReorderPageLayoutTabs.ts index 5c39de81a8..efd7de109b 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useReorderPageLayoutTabs.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useReorderPageLayoutTabs.ts @@ -2,6 +2,7 @@ import { PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS } from '@/page-layout/components/Pag import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout'; import { usePageLayoutDraftState } from '@/page-layout/hooks/usePageLayoutDraftState'; import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { type DropResult } from '@hello-pangea/dnd'; import { useCallback } from 'react'; @@ -31,8 +32,8 @@ export const useReorderPageLayoutTabs = (pageLayoutIdFromProps?: string) => { return false; } - const sortedTabs = [...currentPageLayout.tabs].sort( - (a, b) => a.position - b.position, + const sortedTabs = sortTabsByPosition( + currentPageLayout.tabs.filter((tab) => tab.isActive), ); const draggedTab = sortedTabs.find((tab) => tab.id === draggableId); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutsWithRelationsSelector.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutsWithRelationsSelector.ts index c75887badc..d90cf6f5b8 100644 --- a/packages/twenty-front/src/modules/page-layout/states/pageLayoutsWithRelationsSelector.ts +++ b/packages/twenty-front/src/modules/page-layout/states/pageLayoutsWithRelationsSelector.ts @@ -18,7 +18,6 @@ export const pageLayoutsWithRelationsSelector = createAtomSelector< const allFlatWidgets = get(metadataStoreState, 'pageLayoutWidgets') .current as FlatPageLayoutWidget[]; - const activeFlatTabs = allFlatTabs.filter((tab) => tab.isActive); const activeFlatWidgets = allFlatWidgets.filter( (widget) => widget.isActive, ); @@ -26,7 +25,7 @@ export const pageLayoutsWithRelationsSelector = createAtomSelector< const tabsByPageLayoutId = new Map(); const widgetsByTabId = new Map(); - for (const tab of activeFlatTabs) { + for (const tab of allFlatTabs) { const existing = tabsByPageLayoutId.get(tab.pageLayoutId); if (isDefined(existing)) { diff --git a/packages/twenty-front/src/modules/page-layout/types/PageLayoutAddTabStrategy.ts b/packages/twenty-front/src/modules/page-layout/types/PageLayoutAddTabStrategy.ts new file mode 100644 index 0000000000..4bb6adafad --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/types/PageLayoutAddTabStrategy.ts @@ -0,0 +1,4 @@ +export type PageLayoutAddTabStrategy = { + mode: 'direct' | 'dropdown'; + onCreate: () => void; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutDraftToUpdateInput.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutDraftToUpdateInput.test.ts index 3d40baacf5..8e4028e524 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutDraftToUpdateInput.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutDraftToUpdateInput.test.ts @@ -41,6 +41,7 @@ const makeTab = ( position: 0, pageLayoutId: 'layout-1', applicationId: 'app-1', + isActive: true, layoutMode, widgets, }) as DraftPageLayout['tabs'][number]; diff --git a/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutDraftToUpdateInput.ts b/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutDraftToUpdateInput.ts index 56b185eac5..d3db0877ff 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutDraftToUpdateInput.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutDraftToUpdateInput.ts @@ -48,38 +48,40 @@ export const convertPageLayoutDraftToUpdateInput = ( name: pageLayoutDraft.name, type: pageLayoutDraft.type, objectMetadataId: pageLayoutDraft.objectMetadataId ?? null, - tabs: pageLayoutDraft.tabs.map((tab) => { - const widgets = shouldFilter - ? tab.widgets.filter((widget) => !isDynamicRelationWidget(widget)) - : tab.widgets; + tabs: pageLayoutDraft.tabs + .filter((tab) => tab.isActive) + .map((tab) => { + const widgets = shouldFilter + ? tab.widgets.filter((widget) => !isDynamicRelationWidget(widget)) + : tab.widgets; - return { - id: tab.id, - title: tab.title, - position: tab.position, - layoutMode: tab.layoutMode, - widgets: widgets.map((widget, widgetIndex) => ({ - id: widget.id, - pageLayoutTabId: widget.pageLayoutTabId, - title: widget.title, - type: widget.type, - objectMetadataId: widget.objectMetadataId ?? null, - gridPosition: { - row: widget.gridPosition.row, - column: widget.gridPosition.column, - rowSpan: widget.gridPosition.rowSpan, - columnSpan: widget.gridPosition.columnSpan, - }, - position: buildWidgetPosition( - widget, - widgetIndex, - tab.layoutMode ?? PageLayoutTabLayoutMode.GRID, - ), - configuration: widget.configuration ?? null, - conditionalAvailabilityExpression: - widget.conditionalAvailabilityExpression ?? null, - })), - }; - }), + return { + id: tab.id, + title: tab.title, + position: tab.position, + layoutMode: tab.layoutMode, + widgets: widgets.map((widget, widgetIndex) => ({ + id: widget.id, + pageLayoutTabId: widget.pageLayoutTabId, + title: widget.title, + type: widget.type, + objectMetadataId: widget.objectMetadataId ?? null, + gridPosition: { + row: widget.gridPosition.row, + column: widget.gridPosition.column, + rowSpan: widget.gridPosition.rowSpan, + columnSpan: widget.gridPosition.columnSpan, + }, + position: buildWidgetPosition( + widget, + widgetIndex, + tab.layoutMode ?? PageLayoutTabLayoutMode.GRID, + ), + configuration: widget.configuration ?? null, + conditionalAvailabilityExpression: + widget.conditionalAvailabilityExpression ?? null, + })), + }; + }), }; }; diff --git a/packages/twenty-front/src/modules/page-layout/utils/isReactivatableTab.ts b/packages/twenty-front/src/modules/page-layout/utils/isReactivatableTab.ts new file mode 100644 index 0000000000..842ac6fff6 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/isReactivatableTab.ts @@ -0,0 +1,11 @@ +import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; + +export const isReactivatableTab = ({ + tab, + objectApplicationId, +}: { + tab: PageLayoutTab; + objectApplicationId: string | undefined; +}): boolean => { + return !tab.isActive && tab.applicationId === objectApplicationId; +};