Add record page layout tabs editing (#18702)

- Fix duplication of tabs in dashboards that didn't open the duplicated
tab in the side panel: replaced the logic that used Math.round with an
algorithm that switches the positions of the elements. We will keep
relying on integers, but it will work well in all cases.
- Make dnd work with record page layouts, where there is a pinned tab
- Make tab movements work with pinned tab, too
- Allow user to set a tab as the pinned tab
- Make backend changes to be able to save tabs with `layoutMode`



https://github.com/user-attachments/assets/ce1130fa-71df-49ba-ba2f-6f971e15dd49

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Baptiste Devessier
2026-03-18 15:48:23 +01:00
committed by GitHub
parent f2fa958f20
commit fe4512f80c
77 changed files with 1153 additions and 662 deletions
@@ -1125,6 +1125,7 @@ export type CreatePageLayoutInput = {
};
export type CreatePageLayoutTabInput = {
layoutMode?: InputMaybe<PageLayoutTabLayoutMode>;
pageLayoutId: Scalars['UUID'];
position?: InputMaybe<Scalars['Float']>;
title: Scalars['String'];
@@ -1632,6 +1633,7 @@ export enum FeatureFlagKey {
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED = 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED',
IS_RICH_TEXT_V1_MIGRATED = 'IS_RICH_TEXT_V1_MIGRATED',
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
@@ -5014,6 +5016,7 @@ export type UpdatePageLayoutInput = {
export type UpdatePageLayoutTabInput = {
icon?: InputMaybe<Scalars['String']>;
layoutMode?: InputMaybe<PageLayoutTabLayoutMode>;
position?: InputMaybe<Scalars['Float']>;
title?: InputMaybe<Scalars['String']>;
};
@@ -5021,6 +5024,7 @@ export type UpdatePageLayoutTabInput = {
export type UpdatePageLayoutTabWithWidgetsInput = {
icon?: InputMaybe<Scalars['String']>;
id: Scalars['UUID'];
layoutMode?: InputMaybe<PageLayoutTabLayoutMode>;
position: Scalars['Float'];
title: Scalars['String'];
widgets: Array<UpdatePageLayoutWidgetWithIdInput>;
@@ -2,8 +2,10 @@ import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-c
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
export const CancelDashboardSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
@@ -15,13 +17,25 @@ export const CancelDashboardSingleRecordCommand = () => {
const pageLayoutId = selectedRecord.pageLayoutId;
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
pageLayoutId: pageLayoutId ?? '',
layoutType: PageLayoutType.DASHBOARD,
targetRecordIdentifier: {
id: selectedRecord.id,
targetObjectNameSingular: '',
},
});
const { closeSidePanelMenu } = useSidePanelMenu();
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
const { resetDraftPageLayoutToPersistedPageLayout } =
useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
useResetDraftPageLayoutToPersistedPageLayout({
pageLayoutId,
tabListInstanceId,
});
const handleExecute = () => {
closeSidePanelMenu();
@@ -1,10 +1,13 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrThrow';
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
export const CancelRecordPageLayoutSingleRecordCommand = () => {
const { objectMetadataItem } = useMountedEngineCommandContext();
@@ -15,17 +18,31 @@ export const CancelRecordPageLayoutSingleRecordCommand = () => {
);
}
const recordId = useSelectedRecordIdOrThrow();
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
targetObjectNameSingular: objectMetadataItem.nameSingular,
});
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
pageLayoutId,
layoutType: PageLayoutType.RECORD_PAGE,
targetRecordIdentifier: {
id: recordId,
targetObjectNameSingular: objectMetadataItem.nameSingular,
},
});
const { closeSidePanelMenu } = useSidePanelMenu();
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
const { resetDraftPageLayoutToPersistedPageLayout } =
useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
useResetDraftPageLayoutToPersistedPageLayout({
pageLayoutId,
tabListInstanceId,
});
const handleExecute = () => {
closeSidePanelMenu();
@@ -1,10 +1,13 @@
import { Command } from '@/command-menu-item/display/components/Command';
import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrThrow';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
export const CancelDashboardSingleRecordCommand = () => {
const recordId = useSelectedRecordIdOrThrow();
@@ -13,13 +16,28 @@ export const CancelDashboardSingleRecordCommand = () => {
const pageLayoutId = recordStore?.pageLayoutId;
if (!isDefined(pageLayoutId)) {
throw new Error(
'CancelDashboardSingleRecordCommand requires a valid pageLayoutId from the record store.',
);
}
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
pageLayoutId,
layoutType: PageLayoutType.DASHBOARD,
targetRecordIdentifier: { id: recordId, targetObjectNameSingular: '' },
});
const { closeSidePanelMenu } = useSidePanelMenu();
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
const { resetDraftPageLayoutToPersistedPageLayout } =
useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
useResetDraftPageLayoutToPersistedPageLayout({
pageLayoutId,
tabListInstanceId,
});
const handleClick = () => {
closeSidePanelMenu();
@@ -11,9 +11,10 @@ import { PAGE_LAYOUT_GRID_ITEM_DRAGGING_Z_INDEX } from '@/page-layout/constants/
import { PAGE_LAYOUT_GRID_ITEM_Z_INDEX } from '@/page-layout/constants/PageLayoutGridItemZIndex';
import { PAGE_LAYOUT_GRID_MARGIN } from '@/page-layout/constants/PageLayoutGridMargin';
import { PAGE_LAYOUT_GRID_ROW_HEIGHT } from '@/page-layout/constants/PageLayoutGridRowHeight';
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { usePageLayoutHandleLayoutChange } from '@/page-layout/hooks/usePageLayoutHandleLayoutChange';
import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow';
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentBreakpointComponentState } from '@/page-layout/states/pageLayoutCurrentBreakpointComponentState';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
@@ -24,20 +25,22 @@ import { filterPendingPlaceholderFromLayouts } from '@/page-layout/utils/filterP
import { prepareGridLayoutItemsWithPlaceholders } from '@/page-layout/utils/prepareGridLayoutItemsWithPlaceholders';
import { WidgetPlaceholder } from '@/page-layout/widgets/components/WidgetPlaceholder';
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
import { useMemo, useRef } from 'react';
import {
Responsive,
WidthProvider,
type Layout,
type Layouts,
Responsive,
type ResponsiveProps,
WidthProvider,
} from 'react-grid-layout';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { css } from '@linaria/core';
const disabledTransitionsClass = css`
.react-grid-layout {
@@ -99,6 +102,14 @@ type PageLayoutGridLayoutProps = {
};
export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
);
const tabListInstanceId = useAvailableComponentInstanceIdOrThrow(
TabListComponentInstanceContext,
);
const setPageLayoutCurrentBreakpoint = useSetAtomComponentState(
pageLayoutCurrentBreakpointComponentState,
);
@@ -111,7 +122,10 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
pageLayoutResizingWidgetIdComponentState,
);
const { handleLayoutChange } = usePageLayoutHandleLayoutChange();
const { handleLayoutChange } = usePageLayoutHandleLayoutChange({
pageLayoutId,
tabListInstanceId,
});
const handleLayoutChangeWithoutPendingPlaceholder = (
currentLayout: Layout[],
@@ -1,186 +1,15 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { PageLayoutLeftPanel } from '@/page-layout/components/PageLayoutLeftPanel';
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 { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout';
import { useReorderPageLayoutTabs } from '@/page-layout/hooks/useReorderPageLayoutTabs';
import { PageLayoutMainContent } from '@/page-layout/PageLayoutMainContent';
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
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 { PageLayoutTabsRenderer } from '@/page-layout/components/PageLayoutTabsRenderer';
import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/pageLayoutIsInitializedComponentState';
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 { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { SidePanelPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
const StyledContainer = styled.div<{ hasPinnedTab: boolean }>`
display: grid;
grid-template-columns: ${({ hasPinnedTab }) =>
hasPinnedTab ? `${PAGE_LAYOUT_LEFT_PANEL_CONTAINER_WIDTH}px 1fr` : '1fr'};
grid-template-rows: minmax(0, 1fr);
height: 100%;
width: 100%;
`;
const StyledTabsAndDashboardContainer = styled.div`
display: flex;
flex-direction: column;
overflow: hidden;
`;
const StyledPageLayoutTabListContainer = styled.div`
padding-left: ${themeCssVariables.spacing[2]};
`;
const StyledScrollWrapperContainer = styled.div`
flex: 1;
min-height: 0;
`;
export const PageLayoutRendererContent = () => {
const { currentPageLayout } = useCurrentPageLayout();
const { isInSidePanel, layoutType, targetRecordIdentifier } =
useLayoutRenderingContext();
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const activeTabId = useAtomComponentStateValue(activeTabIdComponentState);
const { createPageLayoutTab } = useCreatePageLayoutTab(currentPageLayout?.id);
const { reorderTabs } = useReorderPageLayoutTabs(currentPageLayout?.id ?? '');
const setPageLayoutTabSettingsOpenTabId = useSetAtomComponentState(
pageLayoutTabSettingsOpenTabIdComponentState,
);
const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel();
const isMobile = useIsMobile();
const metadataStore = useAtomFamilyStateValue(
metadataStoreState,
'objectMetadataItems',
const pageLayoutIsInitialized = useAtomComponentStateValue(
pageLayoutIsInitializedComponentState,
);
const isSystemObject =
(metadataStore.current as ObjectMetadataItem[]).find(
(item) =>
item.nameSingular === targetRecordIdentifier?.targetObjectNameSingular,
)?.isSystem ?? false;
if (!isDefined(currentPageLayout)) {
if (!pageLayoutIsInitialized) {
return null;
}
const handleAddTab =
isPageLayoutInEditMode &&
shouldEnableTabEditingFeatures(currentPageLayout.type)
? () => {
const newTabId = createPageLayoutTab(t`Untitled`);
setPageLayoutTabSettingsOpenTabId(newTabId);
navigatePageLayoutSidePanel({
sidePanelPage: SidePanelPages.PageLayoutTabSettings,
focusTitleInput: true,
});
}
: undefined;
const canEnableTabEditing =
isPageLayoutInEditMode &&
shouldEnableTabEditingFeatures(currentPageLayout.type);
const tabsWithVisibleWidgets = getTabsWithVisibleWidgets({
tabs: currentPageLayout.tabs,
isMobile,
isInSidePanel,
isEditMode: isPageLayoutInEditMode,
});
const SYSTEM_OBJECT_TABS = ['Home', 'Timeline', 'Overview', 'Flow'];
const tabsForCurrentObject = isSystemObject
? tabsWithVisibleWidgets.filter((tab) =>
SYSTEM_OBJECT_TABS.includes(tab.title),
)
: tabsWithVisibleWidgets;
const { tabsToRenderInTabList, pinnedLeftTab } = getTabsByDisplayMode({
tabs: tabsForCurrentObject,
pageLayoutType: currentPageLayout.type,
isMobile,
isInSidePanel,
});
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
pageLayoutId: currentPageLayout.id,
layoutType,
targetRecordIdentifier,
});
const sortedTabs = sortTabsByPosition(tabsToRenderInTabList);
const activeTabExistsInCurrentPageLayout = currentPageLayout.tabs.some(
(tab) => tab.id === activeTabId,
);
return (
<StyledContainer hasPinnedTab={isDefined(pinnedLeftTab)}>
{isDefined(pinnedLeftTab) && (
<PageLayoutLeftPanel pinnedLeftTabId={pinnedLeftTab.id} />
)}
<StyledTabsAndDashboardContainer>
<PageLayoutTabListEffect
tabs={sortedTabs}
componentInstanceId={tabListInstanceId}
defaultTabToFocusOnMobileAndSidePanelId={
currentPageLayout.defaultTabToFocusOnMobileAndSidePanelId ??
undefined
}
/>
{(sortedTabs.length > 1 || isPageLayoutInEditMode) && (
<StyledPageLayoutTabListContainer>
<PageLayoutTabList
tabs={sortedTabs}
behaveAsLinks={!isInSidePanel && !isPageLayoutInEditMode}
componentInstanceId={tabListInstanceId}
onAddTab={handleAddTab}
isReorderEnabled={canEnableTabEditing}
onReorder={canEnableTabEditing ? reorderTabs : undefined}
pageLayoutType={currentPageLayout.type}
/>
</StyledPageLayoutTabListContainer>
)}
<StyledScrollWrapperContainer>
<ScrollWrapper
componentInstanceId={getScrollWrapperInstanceIdFromPageLayoutId(
currentPageLayout.id,
)}
defaultEnableXScroll={false}
>
{isDefined(activeTabId) && activeTabExistsInCurrentPageLayout && (
<PageLayoutMainContent tabId={activeTabId} />
)}
</ScrollWrapper>
</StyledScrollWrapperContainer>
</StyledTabsAndDashboardContainer>
</StyledContainer>
);
return <PageLayoutTabsRenderer />;
};
@@ -1,4 +1,3 @@
import { styled } from '@linaria/react';
import {
DragDropContext,
type DropResult,
@@ -7,6 +6,7 @@ import {
type OnDragUpdateResponder,
type ResponderProvided,
} from '@hello-pangea/dnd';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -25,30 +25,34 @@ import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/con
import { type TabListProps } from '@/ui/layout/tab-list/types/TabListProps';
import { NodeDimension } from '@/ui/utilities/dimensions/components/NodeDimension';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS } from '@/page-layout/components/PageLayoutTabListDroppableIds';
import { PageLayoutTabListReorderableOverflowDropdown } from '@/page-layout/components/PageLayoutTabListReorderableOverflowDropdown';
import { TabListDropdown } from '@/ui/layout/tab-list/components/TabListDropdown';
import { PageLayoutTabListVisibleTabs } from '@/page-layout/components/PageLayoutTabListVisibleTabs';
import { STANDARD_PAGE_LAYOUT_TAB_TITLE_TRANSLATIONS } from '@/page-layout/constants/StandardPageLayoutTabTitleTranslations';
import { useIsCurrentObjectCustom } from '@/page-layout/hooks/useIsCurrentObjectCustom';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutTabListCurrentDragDroppableIdComponentState } from '@/page-layout/states/pageLayoutTabListCurrentDragDroppableIdComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
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 { 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';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { SidePanelPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type PageLayoutType } from '~/generated-metadata/graphql';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import {
FeatureFlagKey,
type PageLayoutType,
} from '~/generated-metadata/graphql';
const StyledContainer = styled.div`
box-sizing: border-box;
@@ -109,6 +113,10 @@ export const PageLayoutTabList = ({
const shouldTranslateTabTitles = !isCustom;
const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
);
const tabsWithIcons: SingleTabProps[] = tabs.map((tab) => ({
id: tab.id,
// TODO: drop once the configuration of all record page layouts has been migrated to the backend.
@@ -260,7 +268,10 @@ export const PageLayoutTabList = ({
(tabId: string) => {
const shouldOpenSettings =
isPageLayoutInEditMode &&
shouldEnableTabEditingFeatures(pageLayoutType);
shouldEnableTabEditingFeatures(
pageLayoutType,
isRecordPageGlobalEditionEnabled,
);
if (shouldOpenSettings && activeTabId === tabId) {
openTabSettings(tabId);
@@ -275,6 +286,7 @@ export const PageLayoutTabList = ({
},
[
isPageLayoutInEditMode,
isRecordPageGlobalEditionEnabled,
pageLayoutType,
activeTabId,
isTabSettingsOpen,
@@ -287,7 +299,10 @@ export const PageLayoutTabList = ({
(tabId: string) => {
const shouldOpenSettings =
isPageLayoutInEditMode &&
shouldEnableTabEditingFeatures(pageLayoutType);
shouldEnableTabEditingFeatures(
pageLayoutType,
isRecordPageGlobalEditionEnabled,
);
if (shouldOpenSettings && activeTabId === tabId) {
openTabSettings(tabId);
@@ -303,6 +318,7 @@ export const PageLayoutTabList = ({
},
[
isPageLayoutInEditMode,
isRecordPageGlobalEditionEnabled,
pageLayoutType,
activeTabId,
isTabSettingsOpen,
@@ -1,4 +1,3 @@
import { styled } from '@linaria/react';
import {
Draggable,
type DraggableProvided,
@@ -6,17 +5,18 @@ import {
type DraggableStateSnapshot,
Droppable,
} from '@hello-pangea/dnd';
import { styled } from '@linaria/react';
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS } from '@/page-layout/components/PageLayoutTabListDroppableIds';
import { PageLayoutTabListDroppableMoreButton } from '@/page-layout/components/PageLayoutTabListDroppableMoreButton';
import { PageLayoutTabMenuItemSelectAvatar } from '@/page-layout/components/PageLayoutTabMenuItemSelectAvatar';
import { PageLayoutTabRenderClone } from '@/page-layout/components/PageLayoutTabRenderClone';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { isPageLayoutTabDraggingComponentState } from '@/page-layout/states/isPageLayoutTabDraggingComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
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 { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@@ -26,10 +26,14 @@ import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useContext } from 'react';
import { SidePanelPages } from 'twenty-shared/types';
import { type PageLayoutType } from '~/generated-metadata/graphql';
import { ThemeContext } from 'twenty-ui/theme-constants';
import {
FeatureFlagKey,
type PageLayoutType,
} from '~/generated-metadata/graphql';
const StyledOverflowDropdownListDraggableWrapper = styled.div`
cursor: grab;
@@ -75,8 +79,16 @@ export const PageLayoutTabListReorderableOverflowDropdown = ({
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
);
const shouldShowEditButton =
isPageLayoutInEditMode && shouldEnableTabEditingFeatures(pageLayoutType);
isPageLayoutInEditMode &&
shouldEnableTabEditingFeatures(
pageLayoutType,
isRecordPageGlobalEditionEnabled,
);
const isPageLayoutTabDragging = useAtomComponentStateValue(
isPageLayoutTabDraggingComponentState,
@@ -0,0 +1,208 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { PageLayoutLeftPanel } from '@/page-layout/components/PageLayoutLeftPanel';
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 { 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 { themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledContainer = styled.div<{ hasPinnedTab: boolean }>`
display: grid;
grid-template-columns: ${({ hasPinnedTab }) =>
hasPinnedTab ? `${PAGE_LAYOUT_LEFT_PANEL_CONTAINER_WIDTH}px 1fr` : '1fr'};
grid-template-rows: minmax(0, 1fr);
height: 100%;
width: 100%;
`;
const StyledTabsAndDashboardContainer = styled.div`
display: flex;
flex-direction: column;
overflow: hidden;
`;
const StyledPageLayoutTabListContainer = styled.div`
padding-left: ${themeCssVariables.spacing[2]};
`;
const StyledScrollWrapperContainer = styled.div`
flex: 1;
min-height: 0;
`;
export const PageLayoutTabsRenderer = () => {
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
const { isInSidePanel, layoutType, targetRecordIdentifier } =
useLayoutRenderingContext();
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const activeTabId = useAtomComponentStateValue(activeTabIdComponentState);
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
pageLayoutId: currentPageLayout.id,
layoutType,
targetRecordIdentifier,
});
const { createPageLayoutTab } = useCreatePageLayoutTab({
pageLayoutId: currentPageLayout.id,
tabListInstanceId,
});
const { reorderRecordPageTabs } = useReorderRecordPageLayoutTabs(
currentPageLayout.id,
);
const setPageLayoutTabSettingsOpenTabId = useSetAtomComponentState(
pageLayoutTabSettingsOpenTabIdComponentState,
);
const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel();
const isMobile = useIsMobile();
const isRecordPageGlobalEditionEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
);
const metadataStore = useAtomFamilyStateValue(
metadataStoreState,
'objectMetadataItems',
);
const isSystemObject =
(metadataStore.current as ObjectMetadataItem[]).find(
(item) =>
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(
currentPageLayout.type,
isRecordPageGlobalEditionEnabled,
);
const tabsWithVisibleWidgets = getTabsWithVisibleWidgets({
tabs: currentPageLayout.tabs,
isMobile,
isInSidePanel,
isEditMode: isPageLayoutInEditMode,
});
const SYSTEM_OBJECT_TABS = ['Home', 'Timeline', 'Overview', 'Flow'];
const tabsForCurrentObject = isSystemObject
? tabsWithVisibleWidgets.filter((tab) =>
SYSTEM_OBJECT_TABS.includes(tab.title),
)
: tabsWithVisibleWidgets;
const { tabsToRenderInTabList, pinnedLeftTab } = getTabsByDisplayMode({
tabs: tabsForCurrentObject,
pageLayoutType: currentPageLayout.type,
isMobile,
isInSidePanel,
});
const sortedTabs = sortTabsByPosition(tabsToRenderInTabList);
const activeTabExistsInCurrentPageLayout = currentPageLayout.tabs.some(
(tab) => tab.id === activeTabId,
);
return (
<StyledContainer hasPinnedTab={isDefined(pinnedLeftTab)}>
{isDefined(pinnedLeftTab) && (
<PageLayoutLeftPanel pinnedLeftTabId={pinnedLeftTab.id} />
)}
<StyledTabsAndDashboardContainer>
<PageLayoutTabListEffect
tabs={sortedTabs}
componentInstanceId={tabListInstanceId}
defaultTabToFocusOnMobileAndSidePanelId={
currentPageLayout.defaultTabToFocusOnMobileAndSidePanelId ??
undefined
}
/>
{(sortedTabs.length > 1 || isPageLayoutInEditMode) && (
<StyledPageLayoutTabListContainer>
<PageLayoutTabList
tabs={sortedTabs}
behaveAsLinks={!isInSidePanel && !isPageLayoutInEditMode}
componentInstanceId={tabListInstanceId}
onAddTab={handleAddTab}
isReorderEnabled={canEnableTabEditing}
onReorder={
canEnableTabEditing
? (result, provided) =>
reorderRecordPageTabs(
result,
provided,
isDefined(pinnedLeftTab),
)
: undefined
}
pageLayoutType={currentPageLayout.type}
/>
</StyledPageLayoutTabListContainer>
)}
<StyledScrollWrapperContainer>
<ScrollWrapper
componentInstanceId={getScrollWrapperInstanceIdFromPageLayoutId(
currentPageLayout.id,
)}
defaultEnableXScroll={false}
>
{isDefined(activeTabId) && activeTabExistsInCurrentPageLayout && (
<PageLayoutMainContent tabId={activeTabId} />
)}
</ScrollWrapper>
</StyledScrollWrapperContainer>
</StyledTabsAndDashboardContainer>
</StyledContainer>
);
};
@@ -2,6 +2,7 @@ import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePag
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
@@ -48,9 +49,12 @@ describe('useCreatePageLayoutGraphWidget', () => {
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const createWidget = useCreatePageLayoutGraphWidget(
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const createWidget = useCreatePageLayoutGraphWidget({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
});
return {
setActiveTabId,
setPageLayoutDraft,
@@ -128,9 +132,12 @@ describe('useCreatePageLayoutGraphWidget', () => {
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const createWidget = useCreatePageLayoutGraphWidget(
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const createWidget = useCreatePageLayoutGraphWidget({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
});
return {
setPageLayoutDraft,
setActiveTabId,
@@ -235,9 +242,12 @@ describe('useCreatePageLayoutGraphWidget', () => {
it('should throw an error when activeTabId is null', () => {
const { result } = renderHook(
() => {
const createWidget = useCreatePageLayoutGraphWidget(
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const createWidget = useCreatePageLayoutGraphWidget({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
});
return { createWidget };
},
{
@@ -1,12 +1,15 @@
import { useCreatePageLayoutTab } from '@/page-layout/hooks/useCreatePageLayoutTab';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { act, renderHook } from '@testing-library/react';
import { useSetAtom } from 'jotai';
import { PageLayoutType } from '~/generated-metadata/graphql';
import {
PageLayoutTabLayoutMode,
PageLayoutType,
} from '~/generated-metadata/graphql';
import {
PAGE_LAYOUT_TEST_INSTANCE_ID,
PageLayoutTestWrapper,
@@ -27,7 +30,12 @@ describe('useCreatePageLayoutTab', () => {
const { result } = renderHook(
() => ({
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
createTab: useCreatePageLayoutTab({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
pageLayoutDraft: useAtomComponentStateValue(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
@@ -36,11 +44,6 @@ describe('useCreatePageLayoutTab', () => {
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
activeTabId: useSetAtom(
activeTabIdComponentState.atomFamily({
instanceId: `${PAGE_LAYOUT_TEST_INSTANCE_ID}-tab-list`,
}),
),
}),
{
wrapper: PageLayoutTestWrapper,
@@ -55,6 +58,9 @@ describe('useCreatePageLayoutTab', () => {
expect(result.current.pageLayoutDraft.tabs[0].id).toBe('mock-uuid');
expect(result.current.pageLayoutDraft.tabs[0].title).toBe('Tab 1');
expect(result.current.pageLayoutDraft.tabs[0].position).toBe(0);
expect(result.current.pageLayoutDraft.tabs[0].layoutMode).toBe(
PageLayoutTabLayoutMode.GRID,
);
expect(result.current.pageLayoutDraft.tabs[0].widgets).toEqual([]);
expect(result.current.pageLayoutCurrentLayouts['mock-uuid']).toEqual({
@@ -69,7 +75,12 @@ describe('useCreatePageLayoutTab', () => {
const { result } = renderHook(
() => ({
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
createTab: useCreatePageLayoutTab({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
pageLayoutDraft: useAtomComponentStateValue(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
@@ -97,7 +108,12 @@ describe('useCreatePageLayoutTab', () => {
const { result } = renderHook(
() => ({
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
createTab: useCreatePageLayoutTab({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
pageLayoutDraft: useAtomComponentStateValue(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
@@ -125,6 +141,53 @@ describe('useCreatePageLayoutTab', () => {
expect(result.current.pageLayoutDraft.tabs[1].title).toBe('Tab 2');
});
it('should default layoutMode to VERTICAL_LIST for record page layouts', () => {
const uuidModule = require('uuid');
uuidModule.v4.mockReturnValue('mock-uuid');
const { result } = renderHook(
() => {
const setPageLayoutDraft = useSetAtomComponentState(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const pageLayoutDraft = useAtomComponentStateValue(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const createTab = useCreatePageLayoutTab({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
});
return { setPageLayoutDraft, pageLayoutDraft, createTab };
},
{
wrapper: PageLayoutTestWrapper,
},
);
act(() => {
result.current.setPageLayoutDraft({
id: 'test-layout',
name: 'Test Layout',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: null,
tabs: [],
});
});
act(() => {
result.current.createTab.createPageLayoutTab();
});
expect(result.current.pageLayoutDraft.tabs).toHaveLength(1);
expect(result.current.pageLayoutDraft.tabs[0].layoutMode).toBe(
PageLayoutTabLayoutMode.VERTICAL_LIST,
);
});
it('should create isolated layouts for multiple tabs', () => {
const uuidModule = require('uuid');
uuidModule.v4
@@ -133,7 +196,12 @@ describe('useCreatePageLayoutTab', () => {
const { result } = renderHook(
() => ({
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
createTab: useCreatePageLayoutTab({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
pageLayoutCurrentLayouts: useAtomComponentStateValue(
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
@@ -179,7 +247,12 @@ describe('useCreatePageLayoutTab', () => {
`${PAGE_LAYOUT_TEST_INSTANCE_ID}-tab-list`,
);
return {
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
createTab: useCreatePageLayoutTab({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
activeTabId: activeTabId,
};
},
@@ -211,7 +284,12 @@ describe('useCreatePageLayoutTab', () => {
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const createTab = useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID);
const createTab = useCreatePageLayoutTab({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
});
return { setPageLayoutDraft, pageLayoutDraft, createTab };
},
{
@@ -1,9 +1,10 @@
import { usePageLayoutHandleLayoutChange } from '@/page-layout/hooks/usePageLayoutHandleLayoutChange';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { act, renderHook } from '@testing-library/react';
import { useSetAtom } from 'jotai';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { usePageLayoutHandleLayoutChange } from '@/page-layout/hooks/usePageLayoutHandleLayoutChange';
import {
PAGE_LAYOUT_TEST_INSTANCE_ID,
PageLayoutTestWrapper,
@@ -13,7 +14,12 @@ describe('usePageLayoutHandleLayoutChange', () => {
it('should update layouts for specific tab only', () => {
const { result } = renderHook(
() => ({
handler: usePageLayoutHandleLayoutChange(PAGE_LAYOUT_TEST_INSTANCE_ID),
handler: usePageLayoutHandleLayoutChange({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
layouts: useAtomComponentStateValue(
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
@@ -56,7 +62,12 @@ describe('usePageLayoutHandleLayoutChange', () => {
it('should isolate layouts between different tabs', () => {
const { result } = renderHook(
() => ({
handler: usePageLayoutHandleLayoutChange(PAGE_LAYOUT_TEST_INSTANCE_ID),
handler: usePageLayoutHandleLayoutChange({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
layouts: useAtomComponentStateValue(
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
@@ -91,7 +102,12 @@ describe('usePageLayoutHandleLayoutChange', () => {
it('should not update layouts when activeTabId is null', () => {
const { result } = renderHook(
() => ({
handler: usePageLayoutHandleLayoutChange(PAGE_LAYOUT_TEST_INSTANCE_ID),
handler: usePageLayoutHandleLayoutChange({
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
layouts: useAtomComponentStateValue(
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
@@ -7,7 +7,6 @@ import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { createDefaultFrontComponentWidget } from '@/page-layout/utils/createDefaultFrontComponentWidget';
import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
@@ -19,9 +18,13 @@ import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import { WidgetType } from '~/generated-metadata/graphql';
export const useCreatePageLayoutFrontComponentWidget = (
pageLayoutIdFromProps?: string,
) => {
export const useCreatePageLayoutFrontComponentWidget = ({
pageLayoutId: pageLayoutIdFromProps,
tabListInstanceId,
}: {
pageLayoutId: string;
tabListInstanceId: string;
}) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
@@ -29,7 +32,7 @@ export const useCreatePageLayoutFrontComponentWidget = (
const activeTabId = useAtomComponentStateValue(
activeTabIdComponentState,
getTabListInstanceIdFromPageLayoutId(pageLayoutId),
tabListInstanceId,
);
const pageLayoutCurrentLayoutsState = useAtomComponentStateCallbackState(
@@ -1,4 +1,3 @@
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
@@ -9,10 +8,10 @@ import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget';
import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
import { getWidgetSize } from '@/page-layout/utils/getWidgetSize';
import { getWidgetTitle } from '@/page-layout/utils/getWidgetTitle';
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
@@ -26,9 +25,13 @@ import {
WidgetType,
} from '~/generated-metadata/graphql';
export const useCreatePageLayoutGraphWidget = (
pageLayoutIdFromProps?: string,
) => {
export const useCreatePageLayoutGraphWidget = ({
pageLayoutId: pageLayoutIdFromProps,
tabListInstanceId,
}: {
pageLayoutId: string;
tabListInstanceId: string;
}) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
@@ -37,8 +40,6 @@ export const useCreatePageLayoutGraphWidget = (
const store = useStore();
const { timeZone, calendarStartDay } = useDateTimeFormat();
const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId);
const pageLayoutDraftState = useAtomComponentStateCallbackState(
pageLayoutDraftComponentState,
pageLayoutId,
@@ -7,7 +7,6 @@ import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { createDefaultIframeWidget } from '@/page-layout/utils/createDefaultIframeWidget';
import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
@@ -19,9 +18,13 @@ import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import { WidgetType } from '~/generated-metadata/graphql';
export const useCreatePageLayoutIframeWidget = (
pageLayoutIdFromProps?: string,
) => {
export const useCreatePageLayoutIframeWidget = ({
pageLayoutId: pageLayoutIdFromProps,
tabListInstanceId,
}: {
pageLayoutId: string;
tabListInstanceId: string;
}) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
@@ -29,7 +32,7 @@ export const useCreatePageLayoutIframeWidget = (
const activeTabId = useAtomComponentStateValue(
activeTabIdComponentState,
getTabListInstanceIdFromPageLayoutId(pageLayoutId),
tabListInstanceId,
);
const pageLayoutCurrentLayoutsState = useAtomComponentStateCallbackState(
@@ -6,7 +6,6 @@ import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLa
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { createDefaultStandaloneRichTextWidget } from '@/page-layout/utils/createDefaultStandaloneRichTextWidget';
import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
@@ -21,16 +20,19 @@ import {
WidgetType,
} from '~/generated-metadata/graphql';
export const useCreatePageLayoutStandaloneRichTextWidget = (
pageLayoutIdFromProps?: string,
) => {
export const useCreatePageLayoutStandaloneRichTextWidget = ({
pageLayoutId: pageLayoutIdFromProps,
tabListInstanceId,
}: {
pageLayoutId: string;
tabListInstanceId: string;
}) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
);
const store = useStore();
const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId);
const pageLayoutCurrentLayoutsState = useAtomComponentStateCallbackState(
pageLayoutCurrentLayoutsComponentState,
@@ -2,8 +2,8 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { getDefaultTabLayoutMode } from '@/page-layout/utils/getDefaultTabLayoutMode';
import { getEmptyTabLayout } from '@/page-layout/utils/getEmptyTabLayout';
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 { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
@@ -12,7 +12,13 @@ import { useStore } from 'jotai';
import { useCallback } from 'react';
import { v4 as uuidv4 } from 'uuid';
export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
export const useCreatePageLayoutTab = ({
pageLayoutId: pageLayoutIdFromProps,
tabListInstanceId,
}: {
pageLayoutId: string;
tabListInstanceId: string;
}) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
@@ -28,7 +34,6 @@ export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
pageLayoutId,
);
const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId);
const setActiveTabId = useSetAtomComponentState(
activeTabIdComponentState,
tabListInstanceId,
@@ -52,6 +57,7 @@ export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
title: title || `Tab ${tabsLength + 1}`,
position: maxPosition + 1,
pageLayoutId: pageLayoutId,
layoutMode: getDefaultTabLayoutMode(pageLayoutDraft.type),
widgets: [],
isOverridden: false,
createdAt: new Date().toISOString(),
@@ -1,7 +1,6 @@
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { removeTabLayouts } from '@/page-layout/utils/removeTabLayouts';
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
@@ -10,7 +9,13 @@ import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/h
import { useStore } from 'jotai';
import { useCallback } from 'react';
export const useDeletePageLayoutTab = (pageLayoutIdFromProps?: string) => {
export const useDeletePageLayoutTab = ({
pageLayoutId: pageLayoutIdFromProps,
tabListInstanceId,
}: {
pageLayoutId: string;
tabListInstanceId: string;
}) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
@@ -27,7 +32,6 @@ export const useDeletePageLayoutTab = (pageLayoutIdFromProps?: string) => {
);
const store = useStore();
const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId);
const activeTabIdAtom = activeTabIdComponentState.atomFamily({
instanceId: tabListInstanceId,
@@ -1,14 +1,12 @@
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { SidePanelPages } from 'twenty-shared/types';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { generateDuplicatedTimestamps } from '@/page-layout/utils/generateDuplicatedTimestamps';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
@@ -16,10 +14,17 @@ import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/h
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { SidePanelPages } from 'twenty-shared/types';
import { appendCopySuffix, isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
export const useDuplicatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
export const useDuplicatePageLayoutTab = ({
pageLayoutId: pageLayoutIdFromProps,
tabListInstanceId,
}: {
pageLayoutId: string;
tabListInstanceId: string;
}) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
@@ -37,7 +42,6 @@ export const useDuplicatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
const store = useStore();
const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId);
const setActiveTabId = useSetAtomComponentState(
activeTabIdComponentState,
tabListInstanceId,
@@ -128,14 +132,14 @@ export const useDuplicatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
setActiveTabId(newTabId);
setPageLayoutTabSettingsOpenTabId(newTabId);
navigatePageLayoutSidePanel({
sidePanelPage: SidePanelPages.PageLayoutTabSettings,
pageTitle: newTab.title,
focusTitleInput: true,
});
setPageLayoutTabSettingsOpenTabId(newTabId);
return newTabId;
},
[
@@ -1,7 +1,6 @@
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
@@ -25,23 +24,25 @@ export const useMovePageLayoutTab = (pageLayoutIdFromProps?: string) => {
store.set(pageLayoutDraftState, (prev) => {
const sorted = sortTabsByPosition(prev.tabs);
const index = sorted.findIndex((t) => t.id === tabId);
if (index <= 0) return prev;
if (index <= 0) {
return prev;
}
const items = sorted.filter((t) => t.id !== tabId);
const destinationIndex = index - 1;
const sourceIndex = index;
const newPosition = calculateNewPosition({
destinationIndex,
sourceIndex,
items,
});
const neighborId = sorted[index - 1].id;
const currentPosition = sorted[index].position;
const neighborPosition = sorted[index - 1].position;
return {
...prev,
tabs: prev.tabs.map((t) =>
t.id === tabId ? { ...t, position: newPosition } : t,
),
tabs: prev.tabs.map((t) => {
if (t.id === tabId) {
return { ...t, position: neighborPosition };
}
if (t.id === neighborId) {
return { ...t, position: currentPosition };
}
return t;
}),
};
});
},
@@ -53,23 +54,25 @@ export const useMovePageLayoutTab = (pageLayoutIdFromProps?: string) => {
store.set(pageLayoutDraftState, (prev) => {
const sorted = sortTabsByPosition(prev.tabs);
const index = sorted.findIndex((t) => t.id === tabId);
if (index < 0 || index >= sorted.length - 1) return prev;
if (index < 0 || index >= sorted.length - 1) {
return prev;
}
const items = sorted.filter((t) => t.id !== tabId);
const destinationIndex = index + 1;
const sourceIndex = index;
const newPosition = calculateNewPosition({
destinationIndex,
sourceIndex,
items,
});
const neighborId = sorted[index + 1].id;
const currentPosition = sorted[index].position;
const neighborPosition = sorted[index + 1].position;
return {
...prev,
tabs: prev.tabs.map((t) =>
t.id === tabId ? { ...t, position: newPosition } : t,
),
tabs: prev.tabs.map((t) => {
if (t.id === tabId) {
return { ...t, position: neighborPosition };
}
if (t.id === neighborId) {
return { ...t, position: currentPosition };
}
return t;
}),
};
});
},
@@ -1,26 +1,28 @@
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { convertLayoutsToWidgets } from '@/page-layout/utils/convertLayoutsToWidgets';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { type Layout, type Layouts } from 'react-grid-layout';
import { useCallback } from 'react';
import { type Layout, type Layouts } from 'react-grid-layout';
import { isDefined } from 'twenty-shared/utils';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { convertLayoutsToWidgets } from '@/page-layout/utils/convertLayoutsToWidgets';
export const usePageLayoutHandleLayoutChange = (
pageLayoutIdFromProps?: string,
) => {
export const usePageLayoutHandleLayoutChange = ({
pageLayoutId: pageLayoutIdFromProps,
tabListInstanceId,
}: {
pageLayoutId: string;
tabListInstanceId: string;
}) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
);
const store = useStore();
const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId);
const pageLayoutCurrentLayoutsState = useAtomComponentStateCallbackState(
pageLayoutCurrentLayoutsComponentState,
@@ -2,7 +2,6 @@ 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 { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { type DropResult } from '@hello-pangea/dnd';
import { useCallback } from 'react';
@@ -41,53 +40,40 @@ export const useReorderPageLayoutTabs = (pageLayoutIdFromProps?: string) => {
return false;
}
if (
const isDropOnMoreButton =
destination.droppableId ===
PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS.MORE_BUTTON
) {
const maxPosition =
sortedTabs.length > 0
? Math.max(...sortedTabs.map((tab) => tab.position))
: 0;
PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS.MORE_BUTTON;
setPageLayoutDraft((prev) => ({
...prev,
tabs: prev.tabs.map((tab) =>
tab.id === draggableId
? { ...tab, position: maxPosition + 1 }
: tab,
),
}));
return true;
}
const tabsWithoutDragged = sortedTabs.filter(
(tab) => tab.id !== draggableId,
);
const orderedIds = sortedTabs
.map((tab) => tab.id)
.filter((id) => id !== draggableId);
const movingBetweenDroppables =
source.droppableId !== destination.droppableId;
const destinationIndexAdjusted =
movingBetweenDroppables && destination.index > source.index
const insertIndex = isDropOnMoreButton
? orderedIds.length
: movingBetweenDroppables && destination.index > source.index
? destination.index - 1
: destination.index;
const newPosition = calculateNewPosition({
destinationIndex: destinationIndexAdjusted,
sourceIndex: source.index,
items: tabsWithoutDragged,
});
orderedIds.splice(insertIndex, 0, draggableId);
const newPositionById = new Map(
orderedIds.map((id, index) => [id, index]),
);
setPageLayoutDraft((prev) => ({
...prev,
tabs: prev.tabs.map((tab) =>
tab.id === draggableId ? { ...tab, position: newPosition } : tab,
),
tabs: prev.tabs.map((tab) => {
const newPosition = newPositionById.get(tab.id);
return isDefined(newPosition)
? { ...tab, position: newPosition }
: tab;
}),
}));
return false;
return isDropOnMoreButton;
},
[currentPageLayout, setPageLayoutDraft],
);
@@ -0,0 +1,45 @@
import { useReorderPageLayoutTabs } from '@/page-layout/hooks/useReorderPageLayoutTabs';
import { type DropResult, type ResponderProvided } from '@hello-pangea/dnd';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useReorderRecordPageLayoutTabs = (
pageLayoutIdFromProps?: string,
) => {
const { reorderTabs } = useReorderPageLayoutTabs(pageLayoutIdFromProps);
const reorderRecordPageTabs = useCallback(
(
result: DropResult,
provided: ResponderProvided,
hasPinnedTab: boolean,
): boolean => {
if (!hasPinnedTab) {
return reorderTabs(result);
}
const { source, destination } = result;
if (!isDefined(destination)) {
return reorderTabs(result);
}
const adjustedResult: DropResult = {
...result,
source: {
...source,
index: source.index + 1,
},
destination: {
...destination,
index: destination.index + 1,
},
};
return reorderTabs(adjustedResult);
},
[reorderTabs],
);
return { reorderRecordPageTabs };
};
@@ -10,7 +10,6 @@ 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 { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
@@ -18,17 +17,19 @@ import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useResetDraftPageLayoutToPersistedPageLayout = (
pageLayoutIdFromProps?: string,
) => {
export const useResetDraftPageLayoutToPersistedPageLayout = ({
pageLayoutId: pageLayoutIdFromProps,
tabListInstanceId,
}: {
pageLayoutId?: string;
tabListInstanceId: string;
}) => {
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
);
const store = useStore();
const tabListComponentInstanceId =
getTabListInstanceIdFromPageLayoutId(componentInstanceId);
const pageLayoutDraftState = useAtomComponentStateCallbackState(
pageLayoutDraftComponentState,
@@ -46,7 +47,7 @@ export const useResetDraftPageLayoutToPersistedPageLayout = (
);
const activeTabId = activeTabIdComponentState.atomFamily({
instanceId: tabListComponentInstanceId,
instanceId: tabListInstanceId,
});
const fieldsWidgetGroupsDraftState = useAtomComponentStateCallbackState(
@@ -0,0 +1,38 @@
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
export const useSetAsPinnedTab = (pageLayoutIdFromProps?: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
);
const pageLayoutDraftState = useAtomComponentStateCallbackState(
pageLayoutDraftComponentState,
pageLayoutId,
);
const store = useStore();
const setAsPinnedTab = useCallback(
(tabId: string) => {
store.set(pageLayoutDraftState, (prev) => {
const minPosition = Math.min(...prev.tabs.map((t) => t.position));
return {
...prev,
tabs: prev.tabs.map((t) =>
t.id === tabId ? { ...t, position: minPosition - 1 } : t,
),
};
});
},
[pageLayoutDraftState, store],
);
return { setAsPinnedTab };
};
@@ -443,4 +443,24 @@ describe('getTabsByDisplayMode', () => {
expect(resultBothTrue.pinnedLeftTab).toBeUndefined();
});
});
describe('pinned tab selection by position', () => {
it('should pin the tab with the lowest position regardless of array order', () => {
const tab1 = { ...createMockTab('tab-1'), position: 2 };
const tab2 = { ...createMockTab('tab-2'), position: 0 };
const tab3 = { ...createMockTab('tab-3'), position: 1 };
const result = getTabsByDisplayMode({
tabs: [tab1, tab2, tab3],
pageLayoutType: PageLayoutType.RECORD_PAGE,
isMobile: false,
isInSidePanel: false,
});
expect(result.pinnedLeftTab?.id).toBe('tab-2');
expect(result.tabsToRenderInTabList).toHaveLength(2);
expect(result.tabsToRenderInTabList[0].id).toBe('tab-3');
expect(result.tabsToRenderInTabList[1].id).toBe('tab-1');
});
});
});
@@ -7,29 +7,54 @@ describe('shouldEnableTabEditingFeatures', () => {
expect(result).toBe(true);
});
it('should return false for RECORD_PAGE layout type', () => {
it('should return false for RECORD_PAGE layout type without flag', () => {
const result = shouldEnableTabEditingFeatures(PageLayoutType.RECORD_PAGE);
expect(result).toBe(false);
});
it('should return true for RECORD_PAGE layout type with flag enabled', () => {
const result = shouldEnableTabEditingFeatures(
PageLayoutType.RECORD_PAGE,
true,
);
expect(result).toBe(true);
});
it('should return false for RECORD_PAGE layout type with flag disabled', () => {
const result = shouldEnableTabEditingFeatures(
PageLayoutType.RECORD_PAGE,
false,
);
expect(result).toBe(false);
});
it('should return false for RECORD_INDEX layout type', () => {
const result = shouldEnableTabEditingFeatures(PageLayoutType.RECORD_INDEX);
expect(result).toBe(false);
});
it('should return false for RECORD_INDEX layout type even with flag', () => {
const result = shouldEnableTabEditingFeatures(
PageLayoutType.RECORD_INDEX,
true,
);
expect(result).toBe(false);
});
describe('behavior validation', () => {
it('should enable tab editing features only for dashboards', () => {
// Dashboards should allow adding tabs and opening settings on click
it('should enable tab editing features only for dashboards and record pages with flag', () => {
expect(shouldEnableTabEditingFeatures(PageLayoutType.DASHBOARD)).toBe(
true,
);
// Record pages should NOT allow adding tabs or opening settings on click
expect(shouldEnableTabEditingFeatures(PageLayoutType.RECORD_PAGE)).toBe(
false,
);
// Record index pages should NOT allow adding tabs or opening settings on click
expect(
shouldEnableTabEditingFeatures(PageLayoutType.RECORD_PAGE, true),
).toBe(true);
expect(shouldEnableTabEditingFeatures(PageLayoutType.RECORD_INDEX)).toBe(
false,
);
@@ -49,6 +49,7 @@ export const convertPageLayoutDraftToUpdateInput = (
id: tab.id,
title: tab.title,
position: tab.position,
layoutMode: tab.layoutMode,
widgets: tab.widgets
.filter((widget) => !isDynamicRelationWidget(widget))
.map((widget, widgetIndex) => ({
@@ -0,0 +1,14 @@
import {
PageLayoutTabLayoutMode,
PageLayoutType,
} from '~/generated-metadata/graphql';
export const getDefaultTabLayoutMode = (
pageLayoutType: PageLayoutType,
): PageLayoutTabLayoutMode => {
if (pageLayoutType === PageLayoutType.RECORD_PAGE) {
return PageLayoutTabLayoutMode.VERTICAL_LIST;
}
return PageLayoutTabLayoutMode.GRID;
};
@@ -1,4 +1,5 @@
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { PageLayoutType } from '~/generated-metadata/graphql';
type GetTabsByDisplayModeParams = {
@@ -32,8 +33,10 @@ export const getTabsByDisplayMode = ({
};
}
const tabsToRenderInTabList = tabs.slice(1);
const pinnedLeftTab = tabs[0];
const sortedTabs = sortTabsByPosition(tabs);
const tabsToRenderInTabList = sortedTabs.slice(1);
const pinnedLeftTab = sortedTabs[0];
return {
tabsToRenderInTabList,
@@ -2,6 +2,18 @@ import { PageLayoutType } from '~/generated-metadata/graphql';
export const shouldEnableTabEditingFeatures = (
pageLayoutType: PageLayoutType,
isRecordPageGlobalEditionEnabled?: boolean,
): boolean => {
return pageLayoutType === PageLayoutType.DASHBOARD;
if (pageLayoutType === PageLayoutType.DASHBOARD) {
return true;
}
if (
pageLayoutType === PageLayoutType.RECORD_PAGE &&
isRecordPageGlobalEditionEnabled
) {
return true;
}
return false;
};
@@ -1,13 +0,0 @@
import { SidePanelPageLayoutInfoContent } from '@/side-panel/components/SidePanelPageLayoutInfoContent';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { isDefined } from 'twenty-shared/utils';
export const SidePanelDashboardPageLayoutInfo = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
if (!isDefined(pageLayoutId)) {
return null;
}
return <SidePanelPageLayoutInfoContent pageLayoutId={pageLayoutId} />;
};
@@ -1,30 +1,13 @@
import { SidePanelDashboardPageLayoutInfo } from '@/side-panel/components/SidePanelDashboardPageLayoutInfo';
import { SidePanelRecordPageLayoutInfo } from '@/side-panel/components/SidePanelRecordPageLayoutInfo';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { SidePanelPageLayoutInfoContent } from '@/side-panel/components/SidePanelPageLayoutInfoContent';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { isDefined } from 'twenty-shared/utils';
export const SidePanelPageLayoutInfo = () => {
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
);
const { pageLayoutId } = usePageLayoutIdFromContextStore();
if (!isDefined(contextStoreCurrentObjectMetadataItemId)) {
throw new Error('Object metadata ID is not defined');
if (!isDefined(pageLayoutId)) {
return null;
}
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: contextStoreCurrentObjectMetadataItemId,
});
const isDashboardContext =
objectMetadataItem.nameSingular === CoreObjectNameSingular.Dashboard;
if (isDashboardContext) {
return <SidePanelDashboardPageLayoutInfo />;
}
return <SidePanelRecordPageLayoutInfo />;
return <SidePanelPageLayoutInfoContent pageLayoutId={pageLayoutId} />;
};
@@ -1,9 +0,0 @@
import { SidePanelPageLayoutInfoContent } from '@/side-panel/components/SidePanelPageLayoutInfoContent';
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
export const SidePanelRecordPageLayoutInfo = () => {
const { pageLayoutId } =
usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord();
return <SidePanelPageLayoutInfoContent pageLayoutId={pageLayoutId} />;
};
@@ -1,5 +1,5 @@
import { ChartFiltersSettingsInitializeStateEffect } from '@/side-panel/pages/page-layout/components/ChartFiltersSettingsInitializeStateEffect';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { type ChartWidget } from '@/side-panel/pages/page-layout/types/ChartWidget';
import { type ChartWidgetConfiguration } from '@/side-panel/pages/page-layout/types/ChartWidgetConfiguration';
@@ -41,7 +41,7 @@ export const ChartFiltersSettings = ({
objectMetadataItemId: objectMetadataItem.id,
});
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
@@ -7,7 +7,7 @@ import { ChartTypeSelectionSection } from '@/side-panel/pages/page-layout/compon
import { CHART_SETTINGS_HEADINGS } from '@/side-panel/pages/page-layout/constants/ChartSettingsHeadings';
import { GRAPH_TYPE_INFORMATION } from '@/side-panel/pages/page-layout/constants/GraphTypeInformation';
import { useChartSettingsValues } from '@/side-panel/pages/page-layout/hooks/useChartSettingsValues';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useGetConfigToUpdateAfterGraphTypeChange } from '@/side-panel/pages/page-layout/hooks/useUpdateGraphTypeConfig';
import { CHART_CONFIGURATION_SETTING_IDS } from '@/side-panel/pages/page-layout/types/ChartConfigurationSettingIds';
@@ -34,7 +34,7 @@ const StyledSidePanelContainer = styled.div`
export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
const { updateSidePanelPageInfo } = useUpdateSidePanelPageInfo();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const { objectMetadataItems } = useObjectMetadataItems();
@@ -2,14 +2,14 @@ import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMeta
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { ChartFiltersSettings } from '@/side-panel/pages/page-layout/components/ChartFiltersSettings';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { isChartWidget } from '@/side-panel/pages/page-layout/utils/isChartWidget';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isDefined } from 'twenty-shared/utils';
export const SidePanelChartFilterSubPage = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const pageLayoutDraft = useAtomComponentStateValue(
pageLayoutDraftComponentState,
@@ -1,7 +1,7 @@
import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration';
import { FieldsConfigurationEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationEditor';
import { FieldsWidgetGroupsDraftInitializationEffect } from '@/page-layout/widgets/fields/components/FieldsWidgetGroupsDraftInitializationEffect';
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { styled } from '@linaria/react';
import { isDefined } from 'twenty-shared/utils';
@@ -21,8 +21,7 @@ const StyledFieldsLayoutContainer = styled.div`
`;
export const SidePanelFieldsLayoutSubPage = () => {
const { pageLayoutId } =
usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const temporaryFieldsConfiguration = useTemporaryFieldsConfiguration();
@@ -3,7 +3,7 @@ import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pa
import { WidgetComponentInstanceContext } from '@/page-layout/widgets/states/contexts/WidgetComponentInstanceContext';
import { ChartSettings } from '@/side-panel/pages/page-layout/components/ChartSettings';
import { WidgetSettingsFooter } from '@/side-panel/pages/page-layout/components/WidgetSettingsFooter';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { isChartWidget } from '@/side-panel/pages/page-layout/utils/isChartWidget';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { styled } from '@linaria/react';
@@ -16,7 +16,7 @@ const StyledContainer = styled.div`
`;
export const SidePanelPageLayoutChartSettings = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const pageLayoutDraft = useAtomComponentStateValue(
pageLayoutDraftComponentState,
@@ -6,7 +6,7 @@ import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory';
import { NewFieldDefaultVisibilityToggle } from '@/side-panel/pages/page-layout/components/NewFieldDefaultVisibilityToggle';
import { WidgetSettingsFooter } from '@/side-panel/pages/page-layout/components/WidgetSettingsFooter';
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
@@ -34,7 +34,7 @@ export const SidePanelPageLayoutFieldsSettings = () => {
const { t } = useLingui();
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
const { pageLayoutId, objectNameSingular } =
usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord();
usePageLayoutIdFromContextStore();
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
@@ -1,5 +1,5 @@
import { WidgetSettingsFooter } from '@/side-panel/pages/page-layout/components/WidgetSettingsFooter';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
@@ -26,7 +26,7 @@ const StyledContainer = styled.div`
`;
export const SidePanelPageLayoutIframeSettings = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
@@ -1,129 +1,18 @@
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { TAB_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/TabSettingsSelectableItemIds';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/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 { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { t } from '@lingui/core/macro';
import { SidePanelPageLayoutTabSettingsContent } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutTabSettingsContent';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { isDefined } from 'twenty-shared/utils';
import {
IconChevronLeft,
IconChevronRight,
IconCopyPlus,
IconTrash,
} from 'twenty-ui/display';
export const SidePanelPageLayoutTabSettings = () => {
const { closeSidePanelMenu } = useSidePanelMenu();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId, recordId } = usePageLayoutIdFromContextStore();
const pageLayoutDraft = useAtomComponentStateValue(
pageLayoutDraftComponentState,
pageLayoutId,
);
const [pageLayoutTabSettingsOpenTabId, setPageLayoutTabSettingsOpenTabId] =
useAtomComponentState(
pageLayoutTabSettingsOpenTabIdComponentState,
pageLayoutId,
);
const { moveLeft, moveRight } = useMovePageLayoutTab(pageLayoutId);
const { deleteTab } = useDeletePageLayoutTab(pageLayoutId);
const { duplicateTab } = useDuplicatePageLayoutTab(pageLayoutId);
if (!isDefined(pageLayoutTabSettingsOpenTabId)) {
if (!isDefined(pageLayoutId)) {
return null;
}
const tabsSorted = sortTabsByPosition(pageLayoutDraft.tabs);
const currentIndex = tabsSorted.findIndex(
(t) => t.id === pageLayoutTabSettingsOpenTabId,
);
if (currentIndex < 0) return null;
const tab = tabsSorted[currentIndex];
const canMoveLeft = currentIndex > 0;
const canMoveRight = currentIndex < tabsSorted.length - 1;
const canDelete = tabsSorted.length > 1;
const handleDelete = () => {
deleteTab(tab.id);
setPageLayoutTabSettingsOpenTabId(null);
closeSidePanelMenu();
};
const selectableItemIds = [
...(canMoveLeft ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT] : []),
...(canMoveRight ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT] : []),
TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE,
...(canDelete ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE] : []),
];
return (
<>
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
<SidePanelGroup heading={t`Settings`}>
{canMoveLeft && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT}
onEnter={() => moveLeft(tab.id)}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT}
Icon={IconChevronLeft}
label={t`Move left`}
onClick={() => moveLeft(tab.id)}
/>
</SelectableListItem>
)}
{canMoveRight && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT}
onEnter={() => moveRight(tab.id)}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT}
Icon={IconChevronRight}
label={t`Move right`}
onClick={() => moveRight(tab.id)}
/>
</SelectableListItem>
)}
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE}
onEnter={() => duplicateTab(tab.id)}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE}
Icon={IconCopyPlus}
label={t`Duplicate`}
onClick={() => duplicateTab(tab.id)}
/>
</SelectableListItem>
{canDelete && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE}
onEnter={handleDelete}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE}
Icon={IconTrash}
label={t`Delete`}
onClick={handleDelete}
/>
</SelectableListItem>
)}
</SidePanelGroup>
</SidePanelList>
</>
<SidePanelPageLayoutTabSettingsContent
pageLayoutId={pageLayoutId}
recordId={recordId}
/>
);
};
@@ -0,0 +1,171 @@
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { useDeletePageLayoutTab } from '@/page-layout/hooks/useDeletePageLayoutTab';
import { useDuplicatePageLayoutTab } from '@/page-layout/hooks/useDuplicatePageLayoutTab';
import { useMovePageLayoutTab } from '@/page-layout/hooks/useMovePageLayoutTab';
import { useSetAsPinnedTab } from '@/page-layout/hooks/useSetAsPinnedTab';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { TAB_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/TabSettingsSelectableItemIds';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import {
IconChevronLeft,
IconChevronRight,
IconCopyPlus,
IconPinned,
IconTrash,
} from 'twenty-ui/display';
import { PageLayoutType } from '~/generated-metadata/graphql';
type SidePanelPageLayoutTabSettingsContentProps = {
pageLayoutId: string;
recordId: string;
};
export const SidePanelPageLayoutTabSettingsContent = ({
pageLayoutId,
recordId,
}: SidePanelPageLayoutTabSettingsContentProps) => {
const { closeSidePanelMenu } = useSidePanelMenu();
const pageLayoutDraft = useAtomComponentStateValue(
pageLayoutDraftComponentState,
pageLayoutId,
);
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
pageLayoutId,
layoutType: pageLayoutDraft.type,
targetRecordIdentifier: { id: recordId, targetObjectNameSingular: '' },
});
const [pageLayoutTabSettingsOpenTabId, setPageLayoutTabSettingsOpenTabId] =
useAtomComponentState(
pageLayoutTabSettingsOpenTabIdComponentState,
pageLayoutId,
);
const { moveLeft, moveRight } = useMovePageLayoutTab(pageLayoutId);
const { deleteTab } = useDeletePageLayoutTab({
pageLayoutId,
tabListInstanceId,
});
const { duplicateTab } = useDuplicatePageLayoutTab({
pageLayoutId,
tabListInstanceId,
});
const { setAsPinnedTab } = useSetAsPinnedTab(pageLayoutId);
if (!isDefined(pageLayoutTabSettingsOpenTabId)) {
return null;
}
const tabsSorted = sortTabsByPosition(pageLayoutDraft.tabs);
const currentIndex = tabsSorted.findIndex(
(t) => t.id === pageLayoutTabSettingsOpenTabId,
);
if (currentIndex < 0) return null;
const tab = tabsSorted[currentIndex];
const isRecordPage = pageLayoutDraft.type === PageLayoutType.RECORD_PAGE;
const hasPinnedTab = isRecordPage && tabsSorted.length > 1;
const canMoveLeft = hasPinnedTab ? currentIndex > 1 : currentIndex > 0;
const canMoveRight = currentIndex < tabsSorted.length - 1;
const canDelete = tabsSorted.length > 1;
const isAlreadyPinned = currentIndex === 0;
const canSetAsPinned =
isRecordPage && !isAlreadyPinned && tabsSorted.length > 1;
const handleDelete = () => {
deleteTab(tab.id);
setPageLayoutTabSettingsOpenTabId(null);
closeSidePanelMenu();
};
const selectableItemIds = [
...(canMoveLeft ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT] : []),
...(canMoveRight ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT] : []),
...(canSetAsPinned ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED] : []),
TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE,
...(canDelete ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE] : []),
];
return (
<>
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
<SidePanelGroup heading={t`Settings`}>
{canMoveLeft && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT}
onEnter={() => moveLeft(tab.id)}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT}
Icon={IconChevronLeft}
label={t`Move left`}
onClick={() => moveLeft(tab.id)}
/>
</SelectableListItem>
)}
{canMoveRight && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT}
onEnter={() => moveRight(tab.id)}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT}
Icon={IconChevronRight}
label={t`Move right`}
onClick={() => moveRight(tab.id)}
/>
</SelectableListItem>
)}
{canSetAsPinned && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED}
onEnter={() => setAsPinnedTab(tab.id)}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED}
Icon={IconPinned}
label={t`Set as pinned tab`}
onClick={() => setAsPinnedTab(tab.id)}
/>
</SelectableListItem>
)}
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE}
onEnter={() => duplicateTab(tab.id)}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE}
Icon={IconCopyPlus}
label={t`Duplicate`}
onClick={() => duplicateTab(tab.id)}
/>
</SelectableListItem>
{canDelete && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE}
onEnter={handleDelete}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE}
Icon={IconTrash}
label={t`Delete`}
onClick={handleDelete}
/>
</SelectableListItem>
)}
</SidePanelGroup>
</SidePanelList>
</>
);
};
@@ -1,11 +1,4 @@
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { getFrontComponentWidgetTypeSelectItemId } from '@/side-panel/pages/page-layout/utils/getFrontComponentWidgetTypeSelectItemId';
import { isExistingWidgetMissingOrDifferentType } from '@/side-panel/pages/page-layout/utils/isExistingWidgetMissingOrDifferentType';
import { FIND_MANY_FRONT_COMPONENTS } from '@/front-components/graphql/queries/findManyFrontComponents';
import { useCreatePageLayoutFrontComponentWidget } from '@/page-layout/hooks/useCreatePageLayoutFrontComponentWidget';
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
@@ -15,6 +8,14 @@ import { useOpportunityDefaultChartConfig } from '@/page-layout/hooks/useOpportu
import { useRemovePageLayoutWidgetAndPreservePosition } from '@/page-layout/hooks/useRemovePageLayoutWidgetAndPreservePosition';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
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 { isExistingWidgetMissingOrDifferentType } from '@/side-panel/pages/page-layout/utils/isExistingWidgetMissingOrDifferentType';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -36,7 +37,7 @@ import {
} from '~/generated-metadata/graphql';
export const SidePanelPageLayoutWidgetTypeSelect = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId, recordId } = usePageLayoutIdFromContextStore();
const { closeSidePanelMenu } = useSidePanelMenu();
@@ -44,17 +45,38 @@ export const SidePanelPageLayoutWidgetTypeSelect = () => {
const { buildBarChartFieldSelection } = useOpportunityDefaultChartConfig();
const { createPageLayoutGraphWidget } =
useCreatePageLayoutGraphWidget(pageLayoutId);
const pageLayoutDraft = useAtomComponentStateValue(
pageLayoutDraftComponentState,
pageLayoutId,
);
const { createPageLayoutIframeWidget } =
useCreatePageLayoutIframeWidget(pageLayoutId);
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
pageLayoutId,
layoutType: pageLayoutDraft.type,
targetRecordIdentifier: { id: recordId, targetObjectNameSingular: '' },
});
const { createPageLayoutGraphWidget } = useCreatePageLayoutGraphWidget({
pageLayoutId,
tabListInstanceId,
});
const { createPageLayoutIframeWidget } = useCreatePageLayoutIframeWidget({
pageLayoutId,
tabListInstanceId,
});
const { createPageLayoutStandaloneRichTextWidget } =
useCreatePageLayoutStandaloneRichTextWidget(pageLayoutId);
useCreatePageLayoutStandaloneRichTextWidget({
pageLayoutId,
tabListInstanceId,
});
const { createPageLayoutFrontComponentWidget } =
useCreatePageLayoutFrontComponentWidget(pageLayoutId);
useCreatePageLayoutFrontComponentWidget({
pageLayoutId,
tabListInstanceId,
});
const { removePageLayoutWidgetAndPreservePosition } =
useRemovePageLayoutWidgetAndPreservePosition(pageLayoutId);
@@ -84,11 +106,6 @@ export const SidePanelPageLayoutWidgetTypeSelect = () => {
pageLayoutId,
);
const pageLayoutDraft = useAtomComponentStateValue(
pageLayoutDraftComponentState,
pageLayoutId,
);
const existingWidget = isDefined(pageLayoutEditingWidgetId)
? pageLayoutDraft.tabs
.flatMap((tab) => tab.widgets)
@@ -6,7 +6,7 @@ import { CommandMenuItemToggle } from '@/command-menu/components/CommandMenuItem
import { SIDE_PANEL_SELECTABLE_LIST_ID } from '@/side-panel/constants/SidePanelSelectableListId';
import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory';
import { useChartSettingsValues } from '@/side-panel/pages/page-layout/hooks/useChartSettingsValues';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateChartSettingInput } from '@/side-panel/pages/page-layout/hooks/useUpdateChartSettingInput';
import { useUpdateChartSettingTextInput } from '@/side-panel/pages/page-layout/hooks/useUpdateChartSettingTextInput';
import { useUpdateChartSettingToggle } from '@/side-panel/pages/page-layout/hooks/useUpdateChartSettingToggle';
@@ -35,7 +35,7 @@ export const ChartSettingItem = ({
objectMetadataId,
configuration,
}: ChartSettingItemProps) => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { closeAnyOpenDropdown } = useCloseAnyOpenDropdown();
const { openDropdown } = useOpenDropdown();
const { setSelectedItemId } = useSelectableList(
@@ -1,4 +1,4 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
@@ -30,7 +30,7 @@ export const ChartAggregateOperationSelectableListItem = ({
label: string;
currentFieldMetadataId: string;
}) => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
@@ -1,7 +1,7 @@
import { ChartAggregateOperationSelectableListItem } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartAggregateOperationSelectableListItem';
import { ChartRatioAggregateOperationSelectableListItem } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartRatioAggregateOperationSelectableListItem';
import { ChartRatioOptionValueSelectionDropdownContent } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartRatioOptionValueSelectionDropdownContent';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
@@ -35,7 +35,7 @@ export const ChartAggregateOperationSelectionDropdownContent = ({
const [searchQuery, setSearchQuery] = useState('');
const [isOptionValueMenuOpen, setIsOptionValueMenuOpen] = useState(false);
const { objectMetadataItems } = useObjectMetadataItems();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const configuration = widgetInEditMode?.configuration;
@@ -1,4 +1,4 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { getChartAxisNameDisplayOptions } from '@/side-panel/pages/page-layout/utils/getChartAxisNameDisplayOptions';
@@ -15,7 +15,7 @@ import { MenuItemSelect } from 'twenty-ui/navigation';
import { AxisNameDisplay } from '~/generated-metadata/graphql';
export const ChartAxisNameSelectionDropdownContent = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const configuration = widgetInEditMode?.configuration;
@@ -1,6 +1,6 @@
import { ChartColorGradientOption } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartColorGradientOption';
import { ChartColorPaletteOption } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartColorPaletteOption';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { type ChartConfiguration } from '@/side-panel/pages/page-layout/types/ChartConfiguration';
@@ -28,7 +28,7 @@ type ColorOption = {
export const ChartColorSelectionDropdownContent = () => {
const [searchQuery, setSearchQuery] = useState('');
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const dropdownId = useAvailableComponentInstanceIdOrThrow(
@@ -1,4 +1,4 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useResetChartDraftFiltersSettings } from '@/side-panel/pages/page-layout/hooks/useResetChartDraftFiltersSettings';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
@@ -32,7 +32,7 @@ export const ChartDataSourceDropdownContent = () => {
useState(false);
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
@@ -1,4 +1,4 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { type ChartConfiguration } from '@/side-panel/pages/page-layout/types/ChartConfiguration';
@@ -75,7 +75,7 @@ const isChartConfiguration = (
export const ChartDateGranularitySelectionDropdownContent = ({
axis,
}: ChartDateGranularitySelectionDropdownContentProps) => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const configuration = widgetInEditMode?.configuration;
@@ -1,5 +1,5 @@
import { ChartAggregateOperationSelectionDropdownContent } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartAggregateOperationSelectionDropdownContent';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
@@ -25,7 +25,7 @@ export const ChartFieldSelectionForAggregateOperationDropdownContent = () => {
const [searchQuery, setSearchQuery] = useState('');
const { objectMetadataItems } = useObjectMetadataItems();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const configuration = widgetInEditMode?.configuration;
@@ -1,6 +1,6 @@
import { ChartGroupByFieldSelectionCompositeFieldView } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionCompositeFieldView';
import { ChartGroupByFieldSelectionRelationFieldView } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionRelationFieldView';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { type ChartConfiguration } from '@/side-panel/pages/page-layout/types/ChartConfiguration';
@@ -51,7 +51,7 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
const { objectMetadataItems } = useObjectMetadataItems();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
@@ -1,6 +1,6 @@
import { type DropResult } from '@hello-pangea/dnd';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
@@ -32,7 +32,7 @@ export const ChartManualSortSubMenuContent = ({
axis,
onBack,
}: ChartManualSortSubMenuContentProps) => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
@@ -1,4 +1,4 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio';
@@ -17,7 +17,7 @@ export const ChartRatioAggregateOperationSelectableListItem = ({
label: string;
onSelect: () => void;
}) => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const dropdownId = useAvailableComponentInstanceIdOrThrow(
@@ -1,4 +1,4 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
@@ -21,7 +21,7 @@ export const ChartRatioOptionBooleanSelectableListItem = ({
label: string;
currentFieldMetadataId: string;
}) => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
@@ -1,4 +1,4 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
@@ -23,7 +23,7 @@ export const ChartRatioOptionSelectSelectableListItem = ({
color: ThemeColor | undefined;
currentFieldMetadataId: string;
}) => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
@@ -1,6 +1,6 @@
import { ChartRatioOptionBooleanSelectableListItem } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartRatioOptionBooleanSelectableListItem';
import { ChartRatioOptionSelectSelectableListItem } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartRatioOptionSelectSelectableListItem';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
@@ -35,7 +35,7 @@ export const ChartRatioOptionValueSelectionDropdownContent = ({
}) => {
const [searchQuery, setSearchQuery] = useState('');
const { objectMetadataItems } = useObjectMetadataItems();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const dropdownId = useAvailableComponentInstanceIdOrThrow(
@@ -3,7 +3,7 @@ import { useState } from 'react';
import { ChartManualSortSubMenuContent } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartManualSortSubMenuContent';
import { AGGREGATE_SORT_BY_OPTIONS } from '@/side-panel/pages/page-layout/constants/AggregateSortByOptions';
import { useGraphGroupBySortOptionLabels } from '@/side-panel/pages/page-layout/hooks/useGraphGroupBySortOptionLabels';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { filterSortOptionsByFieldType } from '@/side-panel/pages/page-layout/utils/filterSortOptionsByFieldType';
@@ -28,7 +28,7 @@ import {
export const ChartSortByGroupByFieldDropdownContent = () => {
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { objectMetadataItems } = useObjectMetadataItems();
const { updateCurrentWidgetConfig } =
@@ -3,7 +3,7 @@ import { useState } from 'react';
import { ChartManualSortSubMenuContent } from '@/side-panel/pages/page-layout/components/dropdown-content/ChartManualSortSubMenuContent';
import { X_SORT_BY_OPTIONS } from '@/side-panel/pages/page-layout/constants/XSortByOptions';
import { useGraphXSortOptionLabels } from '@/side-panel/pages/page-layout/hooks/useGraphXSortOptionLabels';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { filterSortOptionsByFieldType } from '@/side-panel/pages/page-layout/utils/filterSortOptionsByFieldType';
@@ -30,7 +30,7 @@ import {
export const ChartSortBySelectionDropdownContent = () => {
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { objectMetadataItems } = useObjectMetadataItems();
const { updateCurrentWidgetConfig } =
@@ -1,6 +1,7 @@
export const TAB_SETTINGS_SELECTABLE_ITEM_IDS = {
MOVE_LEFT: 'tab-move-left',
MOVE_RIGHT: 'tab-move-right',
SET_AS_PINNED: 'tab-set-as-pinned',
DUPLICATE: 'tab-duplicate',
DELETE: 'tab-delete',
} as const;
@@ -1,31 +0,0 @@
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const usePageLayoutIdFromContextStoreTargetedRecord = () => {
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
contextStoreTargetedRecordsRuleComponentState,
);
if (
!(
contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1
)
) {
throw new Error('Only one recordStore should be selected');
}
const recordId: string = contextStoreTargetedRecordsRule.selectedRecordIds[0];
const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId);
const currentPageLayoutId = useAtomStateValue(currentPageLayoutIdState);
return {
pageLayoutId: recordStore?.pageLayoutId ?? currentPageLayoutId,
};
};
@@ -1,43 +0,0 @@
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { isDefined } from 'twenty-shared/utils';
export const usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord =
() => {
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
contextStoreTargetedRecordsRuleComponentState,
);
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
);
if (!isDefined(contextStoreCurrentObjectMetadataItemId)) {
throw new Error('Object metadata ID is not defined');
}
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: contextStoreCurrentObjectMetadataItemId ?? undefined,
});
if (
!(
contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1
)
) {
throw new Error('Only one record should be selected');
}
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
targetObjectNameSingular: objectMetadataItem.nameSingular,
});
return {
pageLayoutId,
objectNameSingular: objectMetadataItem.nameSingular,
};
};
@@ -0,0 +1,67 @@
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector';
import { getDefaultRecordPageLayoutId } from '@/page-layout/utils/getDefaultRecordPageLayoutId';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const usePageLayoutIdFromContextStore = () => {
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
contextStoreTargetedRecordsRuleComponentState,
);
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
);
if (!isDefined(contextStoreCurrentObjectMetadataItemId)) {
throw new Error('Object metadata ID is not defined');
}
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: contextStoreCurrentObjectMetadataItemId,
});
if (
!(
contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1
)
) {
throw new Error('Only one record should be selected');
}
const recordId: string = contextStoreTargetedRecordsRule.selectedRecordIds[0];
const isDashboardContext =
objectMetadataItem.nameSingular === CoreObjectNameSingular.Dashboard;
const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId);
const currentPageLayoutId = useAtomStateValue(currentPageLayoutIdState);
const recordPageLayout = useAtomFamilySelectorValue(
recordPageLayoutByObjectMetadataIdFamilySelector,
{ objectMetadataId: objectMetadataItem.id },
);
const pageLayoutId = isDashboardContext
? (recordStore?.pageLayoutId ?? currentPageLayoutId)
: isDefined(recordPageLayout)
? recordPageLayout.id
: getDefaultRecordPageLayoutId({
targetObjectNameSingular: objectMetadataItem.nameSingular,
});
return {
pageLayoutId,
recordId,
objectNameSingular: objectMetadataItem.nameSingular,
};
};
@@ -1,4 +1,4 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { getChartFiltersSettingsInstanceId } from '@/side-panel/pages/page-layout/utils/getChartFiltersSettingsInstanceId';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
@@ -11,7 +11,7 @@ import { useStore } from 'jotai';
export const useResetChartDraftFiltersSettings = () => {
const store = useStore();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
@@ -1641,6 +1641,7 @@ enum FeatureFlagKey {
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED
IS_DRAFT_EMAIL_ENABLED
IS_RICH_TEXT_V1_MIGRATED
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
}
type SSOIdentityProvider {
@@ -3445,12 +3446,14 @@ input CreatePageLayoutTabInput {
title: String!
position: Float
pageLayoutId: UUID!
layoutMode: PageLayoutTabLayoutMode = GRID
}
input UpdatePageLayoutTabInput {
title: String
position: Float
icon: String
layoutMode: PageLayoutTabLayoutMode
}
input CreatePageLayoutInput {
@@ -3477,6 +3480,7 @@ input UpdatePageLayoutTabWithWidgetsInput {
title: String!
position: Float!
icon: String
layoutMode: PageLayoutTabLayoutMode = GRID
widgets: [UpdatePageLayoutWidgetWithIdInput!]!
}
@@ -1338,7 +1338,7 @@ export interface FeatureFlag {
__typename: 'FeatureFlag'
}
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ATTACHMENT_MIGRATED' | 'IS_NOTE_TARGET_MIGRATED' | 'IS_TASK_TARGET_MIGRATED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED'
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ATTACHMENT_MIGRATED' | 'IS_NOTE_TARGET_MIGRATED' | 'IS_TASK_TARGET_MIGRATED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED'
export interface SSOIdentityProvider {
id: Scalars['UUID']
@@ -5981,9 +5981,9 @@ export interface GridPositionInput {row: Scalars['Float'],column: Scalars['Float
export interface UpdatePageLayoutWidgetInput {title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null)}
export interface CreatePageLayoutTabInput {title: Scalars['String'],position?: (Scalars['Float'] | null),pageLayoutId: Scalars['UUID']}
export interface CreatePageLayoutTabInput {title: Scalars['String'],position?: (Scalars['Float'] | null),pageLayoutId: Scalars['UUID'],layoutMode?: (PageLayoutTabLayoutMode | null)}
export interface UpdatePageLayoutTabInput {title?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),icon?: (Scalars['String'] | null)}
export interface UpdatePageLayoutTabInput {title?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),icon?: (Scalars['String'] | null),layoutMode?: (PageLayoutTabLayoutMode | null)}
export interface CreatePageLayoutInput {name: Scalars['String'],type?: (PageLayoutType | null),objectMetadataId?: (Scalars['UUID'] | null)}
@@ -5991,7 +5991,7 @@ export interface UpdatePageLayoutInput {name?: (Scalars['String'] | null),type?:
export interface UpdatePageLayoutWithTabsInput {name: Scalars['String'],type: PageLayoutType,objectMetadataId?: (Scalars['UUID'] | null),tabs: UpdatePageLayoutTabWithWidgetsInput[]}
export interface UpdatePageLayoutTabWithWidgetsInput {id: Scalars['UUID'],title: Scalars['String'],position: Scalars['Float'],icon?: (Scalars['String'] | null),widgets: UpdatePageLayoutWidgetWithIdInput[]}
export interface UpdatePageLayoutTabWithWidgetsInput {id: Scalars['UUID'],title: Scalars['String'],position: Scalars['Float'],icon?: (Scalars['String'] | null),layoutMode?: (PageLayoutTabLayoutMode | null),widgets: UpdatePageLayoutWidgetWithIdInput[]}
export interface UpdatePageLayoutWidgetWithIdInput {id: Scalars['UUID'],pageLayoutTabId: Scalars['UUID'],title: Scalars['String'],type: WidgetType,objectMetadataId?: (Scalars['UUID'] | null),gridPosition: GridPositionInput,position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null)}
@@ -8685,7 +8685,8 @@ export const enumFeatureFlagKey = {
IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED: 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' as const,
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED: 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' as const,
IS_DRAFT_EMAIL_ENABLED: 'IS_DRAFT_EMAIL_ENABLED' as const,
IS_RICH_TEXT_V1_MIGRATED: 'IS_RICH_TEXT_V1_MIGRATED' as const
IS_RICH_TEXT_V1_MIGRATED: 'IS_RICH_TEXT_V1_MIGRATED' as const,
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const
}
export const enumRelationType = {
@@ -8824,6 +8824,9 @@ export default {
"pageLayoutId": [
3
],
"layoutMode": [
79
],
"__typename": [
1
]
@@ -8838,6 +8841,9 @@ export default {
"icon": [
1
],
"layoutMode": [
79
],
"__typename": [
1
]
@@ -8900,6 +8906,9 @@ export default {
"icon": [
1
],
"layoutMode": [
79
],
"widgets": [
360
],
@@ -4,4 +4,5 @@ export const FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES = [
'title',
'position',
'icon',
'layoutMode',
] as const satisfies MetadataEntityPropertyName<'pageLayoutTab'>[];
@@ -56,7 +56,8 @@ export const fromCreatePageLayoutTabInputToFlatPageLayoutTabToCreate = ({
widgetIds: [],
widgetUniversalIdentifiers: [],
icon: null,
layoutMode: PageLayoutTabLayoutMode.GRID,
layoutMode:
createPageLayoutTabInput.layoutMode ?? PageLayoutTabLayoutMode.GRID,
overrides: null,
};
};
@@ -1,12 +1,14 @@
import { Field, Float, InputType } from '@nestjs/graphql';
import {
IsEnum,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@@ -26,4 +28,12 @@ export class CreatePageLayoutTabInput {
@IsUUID()
@IsNotEmpty()
pageLayoutId: string;
@Field(() => PageLayoutTabLayoutMode, {
nullable: true,
defaultValue: PageLayoutTabLayoutMode.GRID,
})
@IsEnum(PageLayoutTabLayoutMode)
@IsOptional()
layoutMode?: PageLayoutTabLayoutMode;
}
@@ -3,6 +3,7 @@ import { Field, Float, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsArray,
IsEnum,
IsNotEmpty,
IsNumber,
IsOptional,
@@ -10,6 +11,7 @@ import {
IsUUID,
ValidateNested,
} from 'class-validator';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { UpdatePageLayoutWidgetWithIdInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget-with-id.input';
@@ -35,6 +37,14 @@ export class UpdatePageLayoutTabWithWidgetsInput {
@IsOptional()
icon?: string | null;
@Field(() => PageLayoutTabLayoutMode, {
nullable: true,
defaultValue: PageLayoutTabLayoutMode.GRID,
})
@IsEnum(PageLayoutTabLayoutMode)
@IsOptional()
layoutMode?: PageLayoutTabLayoutMode;
@Field(() => [UpdatePageLayoutWidgetWithIdInput])
@IsArray()
@ValidateNested({ each: true })
@@ -1,6 +1,7 @@
import { Field, Float, InputType } from '@nestjs/graphql';
import { IsNumber, IsOptional, IsString } from 'class-validator';
import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
@InputType()
export class UpdatePageLayoutTabInput {
@@ -18,4 +19,9 @@ export class UpdatePageLayoutTabInput {
@IsString()
@IsOptional()
icon?: string | null;
@Field(() => PageLayoutTabLayoutMode, { nullable: true })
@IsEnum(PageLayoutTabLayoutMode)
@IsOptional()
layoutMode?: PageLayoutTabLayoutMode;
}
@@ -294,7 +294,7 @@ export class PageLayoutUpdateService {
widgetIds: [],
widgetUniversalIdentifiers: [],
icon: null,
layoutMode: PageLayoutTabLayoutMode.GRID,
layoutMode: tabInput.layoutMode ?? PageLayoutTabLayoutMode.GRID,
overrides: null,
};
},
@@ -311,6 +311,7 @@ export class PageLayoutUpdateService {
...existingTab,
title: tabInput.title,
position: tabInput.position,
layoutMode: tabInput.layoutMode ?? existingTab.layoutMode,
updatedAt: now.toISOString(),
};
},
@@ -327,6 +328,7 @@ export class PageLayoutUpdateService {
...existingTab,
title: tabInput.title,
position: tabInput.position,
layoutMode: tabInput.layoutMode ?? existingTab.layoutMode,
deletedAt: null,
updatedAt: now.toISOString(),
};
@@ -243,6 +243,7 @@ describe('WorkspaceEntityManager', () => {
IS_NAVIGATION_MENU_ITEM_ENABLED: false,
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED: false,
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: false,
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: false,
IS_MARKETPLACE_ENABLED: false,
IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED: false,
IS_DRAFT_EMAIL_ENABLED: false,
@@ -1,5 +1,5 @@
import { type QueryRunner } from 'typeorm';
import { FeatureFlagKey } from 'twenty-shared/types';
import { type QueryRunner } from 'typeorm';
const tableName = 'featureFlag';
@@ -105,6 +105,11 @@ export const seedFeatureFlags = async ({
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
workspaceId: workspaceId,
value: true,
},
])
.execute();
};
@@ -19,4 +19,5 @@ export enum FeatureFlagKey {
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED = 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED',
IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED',
IS_RICH_TEXT_V1_MIGRATED = 'IS_RICH_TEXT_V1_MIGRATED',
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED = 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED',
}