Reactivate disabled full tab widgets (#19702)

https://github.com/user-attachments/assets/14a48c7d-e731-4a15-883f-c53beb4943de
This commit is contained in:
Baptiste Devessier
2026-04-15 11:11:55 +02:00
committed by GitHub
parent 3e48be4c31
commit c805a351ab
14 changed files with 383 additions and 110 deletions
@@ -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,
@@ -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<TabListProps, 'tabs'> & {
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 ? (
<StyledAddButton>
<IconButton Icon={IconPlus} size="small" variant="tertiary" />
<TabButton
id="add-tab"
LeftIcon={IconPlus}
title={t`New Tab`}
disableTestId
/>
</StyledAddButton>
) : undefined
}
@@ -405,13 +416,36 @@ export const PageLayoutTabList = ({
</StyledDropdownContainer>
)}
{onAddTab && (
{addTabStrategy?.mode === 'direct' && (
<StyledAddButton>
<IconButton
Icon={IconPlus}
size="small"
variant="tertiary"
onClick={() => onAddTab()}
<TabButton
id="add-tab"
LeftIcon={IconPlus}
title={t`New Tab`}
onClick={() => addTabStrategy.onCreate()}
disableTestId
/>
</StyledAddButton>
)}
{addTabStrategy?.mode === 'dropdown' && (
<StyledAddButton>
<Dropdown
dropdownId={addTabDropdownId}
clickableComponent={
<TabButton
id="add-tab"
LeftIcon={IconPlus}
title={t`New Tab`}
disableTestId
/>
}
dropdownComponents={
<PageLayoutTabListNewTabDropdownContent
onCreate={addTabStrategy.onCreate}
dropdownId={addTabDropdownId}
/>
}
dropdownPlacement="bottom-start"
/>
</StyledAddButton>
)}
@@ -445,13 +479,36 @@ export const PageLayoutTabList = ({
/>
</StyledDropdownContainer>
)}
{onAddTab && (
{addTabStrategy?.mode === 'direct' && (
<StyledAddButton>
<IconButton
Icon={IconPlus}
size="small"
variant="tertiary"
onClick={() => onAddTab()}
<TabButton
id="add-tab"
LeftIcon={IconPlus}
title={t`New Tab`}
onClick={() => addTabStrategy.onCreate()}
disableTestId
/>
</StyledAddButton>
)}
{addTabStrategy?.mode === 'dropdown' && (
<StyledAddButton>
<Dropdown
dropdownId={addTabDropdownId}
clickableComponent={
<TabButton
id="add-tab"
LeftIcon={IconPlus}
title={t`New Tab`}
disableTestId
/>
}
dropdownComponents={
<PageLayoutTabListNewTabDropdownContent
onCreate={addTabStrategy.onCreate}
dropdownId={addTabDropdownId}
/>
}
dropdownPlacement="bottom-start"
/>
</StyledAddButton>
)}
@@ -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 (
<DropdownContent>
<DropdownMenuHeader>{t`New tab`}</DropdownMenuHeader>
<DropdownMenuItemsContainer>
<MenuItem
LeftIcon={IconPlus}
text={t`Empty tab`}
onClick={handleCreateEmptyTab}
/>
</DropdownMenuItemsContainer>
{inactiveTabs.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuSectionLabel label={t`Disabled`} />
<DropdownMenuItemsContainer>
{inactiveTabs.map((tab) => (
<MenuItem
key={tab.id}
LeftIcon={isDefined(tab.icon) ? getIcon(tab.icon) : undefined}
text={getTabTitle(tab.title)}
onClick={() => handleReactivateTab(tab.id)}
/>
))}
</DropdownMenuItemsContainer>
</>
)}
</DropdownContent>
);
};
@@ -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
@@ -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}
@@ -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 };
@@ -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 };
};
@@ -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,
};
};
@@ -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);
@@ -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<string, FlatPageLayoutTab[]>();
const widgetsByTabId = new Map<string, FlatPageLayoutWidget[]>();
for (const tab of activeFlatTabs) {
for (const tab of allFlatTabs) {
const existing = tabsByPageLayoutId.get(tab.pageLayoutId);
if (isDefined(existing)) {
@@ -0,0 +1,4 @@
export type PageLayoutAddTabStrategy = {
mode: 'direct' | 'dropdown';
onCreate: () => void;
};
@@ -41,6 +41,7 @@ const makeTab = (
position: 0,
pageLayoutId: 'layout-1',
applicationId: 'app-1',
isActive: true,
layoutMode,
widgets,
}) as DraftPageLayout['tabs'][number];
@@ -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,
})),
};
}),
};
};
@@ -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;
};