diff --git a/packages/twenty-front/src/modules/app/components/AppRouter.tsx b/packages/twenty-front/src/modules/app/components/AppRouter.tsx index c1845fa131..c5ebc2815b 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouter.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouter.tsx @@ -1,9 +1,7 @@ import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter'; import { currentUserState } from '@/auth/states/currentUserState'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { RouterProvider } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; -import { FeatureFlagKey } from '~/generated/graphql'; export const AppRouter = () => { // We want to disable serverless function settings but keep the code for now @@ -15,17 +13,9 @@ export const AppRouter = () => { (currentUser?.canImpersonate || currentUser?.canAccessFullAdminPanel) ?? false; - const isPageLayoutFeatureFlagEnabled = useIsFeatureEnabled( - FeatureFlagKey.IS_PAGE_LAYOUT_ENABLED, - ); - return ( ); }; diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 5a9523c48c..649d5df1bb 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -287,20 +287,6 @@ const SettingsObjectFieldEdit = lazy(() => ), ); -const SettingsPageLayouts = lazy(() => - import('~/pages/settings/page-layout/SettingsPageLayouts').then((module) => ({ - default: module.SettingsPageLayouts, - })), -); - -const SettingsPageLayoutEdit = lazy(() => - import('~/pages/settings/page-layout/SettingsPageLayoutEdit').then( - (module) => ({ - default: module.SettingsPageLayoutEdit, - }), - ), -); - const SettingsSecurity = lazy(() => import('~/pages/settings/security/SettingsSecurity').then((module) => ({ default: module.SettingsSecurity, @@ -386,13 +372,11 @@ const SettingsRoleAddObjectLevel = lazy(() => type SettingsRoutesProps = { isFunctionSettingsEnabled?: boolean; isAdminPageEnabled?: boolean; - isPageLayoutFeatureFlagEnabled?: boolean; }; export const SettingsRoutes = ({ isFunctionSettingsEnabled, isAdminPageEnabled, - isPageLayoutFeatureFlagEnabled, }: SettingsRoutesProps) => ( }> @@ -614,22 +598,7 @@ export const SettingsRoutes = ({ /> )} - {isPageLayoutFeatureFlagEnabled && ( - <> - } - /> - } - /> - } - /> - - )} + createBrowserRouter( createRoutesFromElements( @@ -70,7 +69,6 @@ export const useCreateAppRouter = ( } /> diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx index 76cef582cc..cd43aeec87 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx @@ -6,14 +6,10 @@ import { COMMAND_MENU_PAGES_CONFIG } from '@/command-menu/constants/CommandMenuP import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext'; -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; -import { useLocation } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; -import { SettingsPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; const StyledCommandMenuContent = styled.div` @@ -24,8 +20,6 @@ const StyledCommandMenuContent = styled.div` export const CommandMenuRouter = () => { const commandMenuPage = useRecoilValue(commandMenuPageState); const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState); - const location = useLocation(); - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const commandMenuPageComponent = isDefined(commandMenuPage) ? ( COMMAND_MENU_PAGES_CONFIG.get(commandMenuPage) @@ -35,13 +29,6 @@ export const CommandMenuRouter = () => { const theme = useTheme(); - const isSettingsPage = location.pathname.includes(SettingsPath.PageLayout); - const objectMetadataItemOverride = isSettingsPage - ? objectMetadataItems.find( - (item) => item.nameSingular === CoreObjectNameSingular.Dashboard, - ) - : undefined; - return ( @@ -64,7 +51,6 @@ export const CommandMenuRouter = () => { isInRightDrawer={true} displayType="listItem" actionMenuType="command-menu" - objectMetadataItemOverride={objectMetadataItemOverride} > {commandMenuPageComponent} diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx index 2c5d8a17fd..487a376b37 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx @@ -1,4 +1,5 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; import { useCreatePageLayoutWidget } from '@/page-layout/hooks/useCreatePageLayoutWidget'; import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets'; import styled from '@emotion/styled'; @@ -57,7 +58,10 @@ const graphTypeOptions = [ export const CommandMenuPageLayoutGraphTypeSelect = () => { const { closeCommandMenu } = useCommandMenu(); - const { createPageLayoutWidget } = useCreatePageLayoutWidget(); + + const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord(); + + const { createPageLayoutWidget } = useCreatePageLayoutWidget(pageLayoutId); const handleSelectGraphType = (graphType: GraphType) => { createPageLayoutWidget(WidgetType.GRAPH, graphType); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx index 4160e23511..3eb1d31933 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx @@ -1,13 +1,15 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput'; import { useCreatePageLayoutIframeWidget } from '@/page-layout/hooks/useCreatePageLayoutIframeWidget'; import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; -import { pageLayoutDraftState } from '@/page-layout/states/pageLayoutDraftState'; -import { pageLayoutEditingWidgetIdState } from '@/page-layout/states/pageLayoutEditingWidgetIdState'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import styled from '@emotion/styled'; import { isString } from '@sniptt/guards'; import { useState } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; import { isValidUrl } from 'twenty-shared/utils'; import { Button } from 'twenty-ui/input'; @@ -33,21 +35,37 @@ const StyledButtonContainer = styled.div` export const CommandMenuPageLayoutIframeConfig = () => { const { closeCommandMenu } = useCommandMenu(); - const { createPageLayoutIframeWidget } = useCreatePageLayoutIframeWidget(); - const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(); + + const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord(); + + const { createPageLayoutIframeWidget } = + useCreatePageLayoutIframeWidget(pageLayoutId); + + const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId); + const [pageLayoutEditingWidgetId, setPageLayoutEditingWidgetId] = - useRecoilState(pageLayoutEditingWidgetIdState); - const pageLayoutDraft = useRecoilValue(pageLayoutDraftState); + useRecoilComponentState( + pageLayoutEditingWidgetIdComponentState, + pageLayoutId, + ); + const pageLayoutDraft = useRecoilComponentValue( + pageLayoutDraftComponentState, + pageLayoutId, + ); const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets); + const editingWidget = allWidgets.find( (w) => w.id === pageLayoutEditingWidgetId, ); const isEditMode = !!editingWidget; const [title, setTitle] = useState(editingWidget?.title || ''); + const configUrl = editingWidget?.configuration?.url; + const [url, setUrl] = useState(isString(configUrl) ? configUrl : ''); + const [urlError, setUrlError] = useState(''); const validateUrl = (urlString: string): boolean => { @@ -84,6 +102,7 @@ export const CommandMenuPageLayoutIframeConfig = () => { url: url.trim(), }, }); + setPageLayoutEditingWidgetId(null); } else { createPageLayoutIframeWidget(title.trim(), url.trim()); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx index 53666089a3..22b9631213 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx @@ -1,9 +1,10 @@ -import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; +import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu'; +import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { WidgetType } from '@/page-layout/mocks/mockWidgets'; -import { pageLayoutDraggedAreaState } from '@/page-layout/states/pageLayoutDraggedAreaState'; +import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import styled from '@emotion/styled'; -import { useSetRecoilState } from 'recoil'; import { IconChartPie, IconFrame, IconList } from 'twenty-ui/display'; import { MenuItemCommand } from 'twenty-ui/navigation'; @@ -50,27 +51,31 @@ const widgetTypeOptions = [ ]; export const CommandMenuPageLayoutWidgetTypeSelect = () => { - const { navigateCommandMenu } = useNavigateCommandMenu(); - const setPageLayoutDraggedArea = useSetRecoilState( - pageLayoutDraggedAreaState, + const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord(); + + const setPageLayoutDraggedArea = useSetRecoilComponentState( + pageLayoutDraggedAreaComponentState, + pageLayoutId, ); + const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu(); + const handleSelectWidget = (widgetType: WidgetType) => { switch (widgetType) { - case WidgetType.GRAPH: - navigateCommandMenu({ - page: CommandMenuPages.PageLayoutGraphTypeSelect, - pageTitle: 'Select Graph Type', - pageIcon: IconChartPie, + case WidgetType.GRAPH: { + navigatePageLayoutCommandMenu({ + commandMenuPage: CommandMenuPages.PageLayoutGraphTypeSelect, }); + break; - case WidgetType.IFRAME: - navigateCommandMenu({ - page: CommandMenuPages.PageLayoutIframeConfig, - pageTitle: 'Configure iFrame', - pageIcon: IconFrame, + } + case WidgetType.IFRAME: { + navigatePageLayoutCommandMenu({ + commandMenuPage: CommandMenuPages.PageLayoutIframeConfig, }); + break; + } default: setPageLayoutDraggedArea(null); break; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu.ts new file mode 100644 index 0000000000..edb678df24 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu.ts @@ -0,0 +1,42 @@ +import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; +import { getPageLayoutIcon } from '@/command-menu/pages/page-layout/utils/getPageLayoutIcon'; +import { getPageLayoutPageTitle } from '@/command-menu/pages/page-layout/utils/getPageLayoutPageTitle'; +import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { type IconComponent } from 'twenty-ui/display'; + +type NavigatePageLayoutCommandMenuProps = { + commandMenuPage: + | CommandMenuPages.PageLayoutWidgetTypeSelect + | CommandMenuPages.PageLayoutGraphTypeSelect + | CommandMenuPages.PageLayoutIframeConfig; + pageTitle?: string; + pageIcon?: IconComponent; +}; + +export const useNavigatePageLayoutCommandMenu = () => { + const { navigateCommandMenu } = useNavigateCommandMenu(); + + const navigatePageLayoutCommandMenu = useRecoilCallback(() => { + return ({ + commandMenuPage, + pageTitle, + pageIcon, + }: NavigatePageLayoutCommandMenuProps) => { + navigateCommandMenu({ + page: commandMenuPage, + pageTitle: isDefined(pageTitle) + ? pageTitle + : getPageLayoutPageTitle(commandMenuPage), + pageIcon: isDefined(pageIcon) + ? pageIcon + : getPageLayoutIcon(commandMenuPage), + }); + }; + }, [navigateCommandMenu]); + + return { + navigatePageLayoutCommandMenu, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord.ts new file mode 100644 index 0000000000..eee8685cb4 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord.ts @@ -0,0 +1,25 @@ +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useRecoilValue } from 'recoil'; + +export const usePageLayoutIdFromContextStoreTargetedRecord = () => { + const targetedRecordsRule = useRecoilComponentValue( + contextStoreTargetedRecordsRuleComponentState, + ); + + if ( + !( + targetedRecordsRule.mode === 'selection' && + targetedRecordsRule.selectedRecordIds.length === 1 + ) + ) { + throw new Error('Only one record should be selected'); + } + + const recordId: string = targetedRecordsRule.selectedRecordIds[0]; + + const record = useRecoilValue(recordStoreFamilyState(recordId)); + + return { pageLayoutId: record?.pageLayoutId }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage.ts new file mode 100644 index 0000000000..a6ac401b19 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage.ts @@ -0,0 +1,6 @@ +import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; + +export type PageLayoutCommandMenuPage = + | CommandMenuPages.PageLayoutWidgetTypeSelect + | CommandMenuPages.PageLayoutGraphTypeSelect + | CommandMenuPages.PageLayoutIframeConfig; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutIcon.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutIcon.ts new file mode 100644 index 0000000000..ee3de81e40 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutIcon.ts @@ -0,0 +1,17 @@ +import { type PageLayoutCommandMenuPage } from '@/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { assertUnreachable } from 'twenty-shared/utils'; +import { IconAppWindow, IconChartPie, IconFrame } from 'twenty-ui/display'; + +export const getPageLayoutIcon = (page: PageLayoutCommandMenuPage) => { + switch (page) { + case CommandMenuPages.PageLayoutWidgetTypeSelect: + return IconAppWindow; + case CommandMenuPages.PageLayoutGraphTypeSelect: + return IconChartPie; + case CommandMenuPages.PageLayoutIframeConfig: + return IconFrame; + default: + assertUnreachable(page); + } +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutPageTitle.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutPageTitle.ts new file mode 100644 index 0000000000..6f85f874c1 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutPageTitle.ts @@ -0,0 +1,17 @@ +import { type PageLayoutCommandMenuPage } from '@/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { t } from '@lingui/core/macro'; +import { assertUnreachable } from 'twenty-shared/utils'; + +export const getPageLayoutPageTitle = (page: PageLayoutCommandMenuPage) => { + switch (page) { + case CommandMenuPages.PageLayoutWidgetTypeSelect: + return t`Add Widget`; + case CommandMenuPages.PageLayoutGraphTypeSelect: + return t`Select Graph Type`; + case CommandMenuPages.PageLayoutIframeConfig: + return t`Configure iFrame`; + default: + assertUnreachable(page); + } +}; diff --git a/packages/twenty-front/src/modules/dashboards/components/DashboardRenderer.tsx b/packages/twenty-front/src/modules/dashboards/components/DashboardRenderer.tsx index 82372332fd..e75d43fea8 100644 --- a/packages/twenty-front/src/modules/dashboards/components/DashboardRenderer.tsx +++ b/packages/twenty-front/src/modules/dashboards/components/DashboardRenderer.tsx @@ -1,36 +1,22 @@ import { type Dashboard } from '@/dashboards/components/types/Dashboard'; -import { FIND_ONE_PAGE_LAYOUT } from '@/dashboards/graphql/queries/findOnePageLayout'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer'; -import { normalizePageLayoutData } from '@/page-layout/utils/normalizePageLayoutData'; -import { useQuery } from '@apollo/client'; +import { isDefined } from 'twenty-shared/utils'; type DashboardRendererProps = { recordId: string; }; export const DashboardRenderer = ({ recordId }: DashboardRendererProps) => { - const { record: dashboard, loading: dashboardLoading } = - useFindOneRecord({ - objectNameSingular: CoreObjectNameSingular.Dashboard, - objectRecordId: recordId, - }); - - const { data } = useQuery(FIND_ONE_PAGE_LAYOUT, { - variables: { - id: dashboard?.pageLayoutId, - }, - skip: dashboardLoading || !dashboard?.pageLayoutId, + const { record: dashboard } = useFindOneRecord({ + objectNameSingular: CoreObjectNameSingular.Dashboard, + objectRecordId: recordId, }); - const pageLayout = data?.getPageLayout; - - if (!pageLayout) { + if (!isDefined(dashboard)) { return null; } - const normalizedPageLayout = normalizePageLayoutData(pageLayout); - - return ; + return ; }; diff --git a/packages/twenty-front/src/modules/dashboards/graphql/fragments/pageLayoutTabFragment.ts b/packages/twenty-front/src/modules/dashboards/graphql/fragments/pageLayoutTabFragment.ts index 1e5fa09aef..de08b3d1a0 100644 --- a/packages/twenty-front/src/modules/dashboards/graphql/fragments/pageLayoutTabFragment.ts +++ b/packages/twenty-front/src/modules/dashboards/graphql/fragments/pageLayoutTabFragment.ts @@ -10,5 +10,6 @@ export const PAGE_LAYOUT_TAB_FRAGMENT = gql` widgets { ...PageLayoutWidgetFragment } + pageLayoutId } `; diff --git a/packages/twenty-front/src/modules/dashboards/graphql/fragments/pageLayoutWidgetFragment.ts b/packages/twenty-front/src/modules/dashboards/graphql/fragments/pageLayoutWidgetFragment.ts index ea60ebf84f..5a0a7c976d 100644 --- a/packages/twenty-front/src/modules/dashboards/graphql/fragments/pageLayoutWidgetFragment.ts +++ b/packages/twenty-front/src/modules/dashboards/graphql/fragments/pageLayoutWidgetFragment.ts @@ -13,5 +13,6 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql` rowSpan } configuration + pageLayoutTabId } `; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx new file mode 100644 index 0000000000..a123cec828 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx @@ -0,0 +1,154 @@ +import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { PageLayoutGridLayoutDragSelector } from '@/page-layout/components/PageLayoutGridLayoutDragSelector'; +import { PageLayoutGridOverlay } from '@/page-layout/components/PageLayoutGridOverlay'; +import { EMPTY_LAYOUT } from '@/page-layout/constants/EmptyLayout'; +import { + PAGE_LAYOUT_CONFIG, + type PageLayoutBreakpoint, +} from '@/page-layout/constants/PageLayoutBreakpoints'; +import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout'; +import { usePageLayoutHandleLayoutChange } from '@/page-layout/hooks/usePageLayoutHandleLayoutChange'; +import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; +import { pageLayoutCurrentBreakpointComponentState } from '@/page-layout/states/pageLayoutCurrentBreakpointComponentState'; +import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; +import { WidgetPlaceholder } from '@/page-layout/widgets/components/WidgetPlaceholder'; +import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer'; +import { type Widget } from '@/page-layout/widgets/types/Widget'; +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import styled from '@emotion/styled'; +import { useRef } from 'react'; +import { + Responsive, + WidthProvider, + type ResponsiveProps, +} from 'react-grid-layout'; +import { isDefined } from 'twenty-shared/utils'; + +const StyledGridContainer = styled.div` + background: ${({ theme }) => theme.background.primary}; + box-sizing: border-box; + flex: 1; + min-height: 100%; + overflow-y: auto; + overflow-x: hidden; + position: relative; + padding: ${({ theme }) => theme.spacing(2)}; + width: 100%; + user-select: none; + + .react-grid-placeholder { + background: ${({ theme }) => theme.adaptiveColors.blue3} !important; + + border-radius: ${({ theme }) => theme.border.radius.sm}; + } + + .react-grid-item:not(.react-draggable-dragging) { + user-select: auto; + } +`; + +type ExtendedResponsiveProps = ResponsiveProps & { + maxCols?: number; + preventCollision?: boolean; +}; + +const ResponsiveGridLayout = WidthProvider( + Responsive, +) as React.ComponentType; + +export const PageLayoutGridLayout = () => { + const setPageLayoutCurrentBreakpoint = useSetRecoilComponentState( + pageLayoutCurrentBreakpointComponentState, + ); + + const { handleLayoutChange } = usePageLayoutHandleLayoutChange(); + + const gridContainerRef = useRef(null); + + const isPageLayoutInEditMode = useRecoilComponentValue( + isPageLayoutInEditModeComponentState, + ); + + const pageLayoutCurrentLayouts = useRecoilComponentValue( + pageLayoutCurrentLayoutsComponentState, + ); + + const activeTabId = useRecoilComponentValue(activeTabIdComponentState); + + const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu(); + + const { currentPageLayout } = useCurrentPageLayout(); + + if (!isDefined(activeTabId) || !isDefined(currentPageLayout)) { + return null; + } + + const activeTabWidgets = currentPageLayout?.tabs.find( + (tab) => tab.id === activeTabId, + )?.widgets; + + const isLayoutEmpty = + !isDefined(activeTabWidgets) || activeTabWidgets.length === 0; + + const layouts = isLayoutEmpty + ? EMPTY_LAYOUT + : pageLayoutCurrentLayouts[activeTabId] || EMPTY_LAYOUT; + + return ( + <> + + {isPageLayoutInEditMode && ( + <> + + + + )} + + setPageLayoutCurrentBreakpoint( + newBreakpoint as PageLayoutBreakpoint, + ) + } + > + {isLayoutEmpty ? ( +
+ { + navigatePageLayoutCommandMenu({ + commandMenuPage: + CommandMenuPages.PageLayoutWidgetTypeSelect, + }); + }} + /> +
+ ) : ( + activeTabWidgets?.map((widget) => ( +
+ +
+ )) + )} +
+
+ + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayoutDragSelector.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayoutDragSelector.tsx new file mode 100644 index 0000000000..0d1571a9b0 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayoutDragSelector.tsx @@ -0,0 +1,24 @@ +import { useChangePageLayoutDragSelection } from '@/page-layout/hooks/useChangePageLayoutDragSelection'; +import { useEndPageLayoutDragSelection } from '@/page-layout/hooks/useEndPageLayoutDragSelection'; +import { useStartPageLayoutDragSelection } from '@/page-layout/hooks/useStartPageLayoutDragSelection'; +import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; +import { type RefObject } from 'react'; + +export const PageLayoutGridLayoutDragSelector = ({ + gridContainerRef, +}: { + gridContainerRef: RefObject; +}) => { + const { startPageLayoutDragSelection } = useStartPageLayoutDragSelection(); + const { changePageLayoutDragSelection } = useChangePageLayoutDragSelection(); + const { endPageLayoutDragSelection } = useEndPageLayoutDragSelection(); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridOverlay.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridOverlay.tsx new file mode 100644 index 0000000000..a581d4762a --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridOverlay.tsx @@ -0,0 +1,101 @@ +import { type PageLayoutBreakpoint } from '@/page-layout/constants/PageLayoutBreakpoints'; +import { pageLayoutCurrentBreakpointComponentState } from '@/page-layout/states/pageLayoutCurrentBreakpointComponentState'; +import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; +import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState'; +import { calculateGridCellPosition } from '@/page-layout/utils/calculateGridCellPosition'; +import { calculateTotalGridRows } from '@/page-layout/utils/calculateTotalGridRows'; +import { generateCellId } from '@/page-layout/utils/generateCellId'; +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import styled from '@emotion/styled'; +import { useMemo } from 'react'; + +const StyledGridOverlay = styled.div<{ + isDragSelecting?: boolean; + breakpoint: PageLayoutBreakpoint; +}>` + position: absolute; + top: ${({ theme }) => theme.spacing(2)}; + left: ${({ theme }) => theme.spacing(2)}; + right: ${({ theme }) => theme.spacing(2)}; + bottom: ${({ theme }) => theme.spacing(2)}; + display: grid; + grid-template-columns: ${({ breakpoint }) => + breakpoint === 'mobile' ? '1fr' : 'repeat(12, 1fr)'}; + grid-auto-rows: 55px; + gap: ${({ theme }) => theme.spacing(2)}; + pointer-events: ${({ isDragSelecting }) => + isDragSelecting ? 'auto' : 'none'}; + z-index: 0; +`; + +const StyledGridCell = styled.div<{ isSelected?: boolean }>` + background: ${({ isSelected, theme }) => + isSelected ? theme.adaptiveColors.blue1 : 'transparent'}; + border: 1px solid + ${({ theme, isSelected }) => + isSelected ? theme.adaptiveColors.blue3 : theme.border.color.light}; + border-radius: ${({ theme }) => theme.border.radius.md}; + transition: background-color 0.3s ease; + + &:hover { + background: ${({ theme }) => theme.background.transparent.lighter}; + border-color: ${({ theme }) => theme.border.color.medium}; + } +`; + +export const PageLayoutGridOverlay = () => { + const pageLayoutCurrentBreakpoint = useRecoilComponentValue( + pageLayoutCurrentBreakpointComponentState, + ); + + const pageLayoutSelectedCells = useRecoilComponentValue( + pageLayoutSelectedCellsComponentState, + ); + + const pageLayoutCurrentLayouts = useRecoilComponentValue( + pageLayoutCurrentLayoutsComponentState, + ); + + const activeTabId = useRecoilComponentValue(activeTabIdComponentState); + + const numberOfRows = useMemo(() => { + const currentTabLayouts = pageLayoutCurrentLayouts[activeTabId ?? ''] || { + desktop: [], + mobile: [], + }; + return calculateTotalGridRows(currentTabLayouts); + }, [pageLayoutCurrentLayouts, activeTabId]); + + const isPageLayoutCurrentBreakpointMobile = + pageLayoutCurrentBreakpoint === 'mobile'; + + const numberOfColumns = pageLayoutCurrentBreakpoint === 'mobile' ? 1 : 12; + + return ( + + {Array.from( + { + length: numberOfColumns * numberOfRows, + }, + (_, i) => { + const { column, row } = calculateGridCellPosition({ + index: i, + numberOfColumns, + }); + const cellId = generateCellId(column, row); + return ( + + ); + }, + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationEffect.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationEffect.tsx deleted file mode 100644 index b99c5e0609..0000000000 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationEffect.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { useEffect, useState } from 'react'; -import { useRecoilCallback, useRecoilValue } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; -import { v4 as uuidv4 } from 'uuid'; -import { PageLayoutType } from '~/generated/graphql'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; -import { - pageLayoutCurrentLayoutsState, - type TabLayouts, -} from '../states/pageLayoutCurrentLayoutsState'; -import { pageLayoutDraftState } from '../states/pageLayoutDraftState'; -import { pageLayoutPersistedState } from '../states/pageLayoutPersistedState'; -import { savedPageLayoutsState } from '../states/savedPageLayoutsState'; -import { type PageLayoutWithData } from '../types/pageLayoutTypes'; - -type PageLayoutInitializationEffectProps = { - layoutId: string | undefined; - isEditMode: boolean; - pageLayout?: PageLayoutWithData; -}; - -export const PageLayoutInitializationEffect = ({ - layoutId, - isEditMode, - pageLayout, -}: PageLayoutInitializationEffectProps) => { - const [isInitialized, setIsInitialized] = useState(false); - const savedPageLayouts = useRecoilValue(savedPageLayoutsState); - - const initializePageLayout = useRecoilCallback( - ({ set, snapshot }) => - (layout: PageLayoutWithData | undefined) => { - const currentPersisted = getSnapshotValue( - snapshot, - pageLayoutPersistedState, - ); - - if (isDefined(layout)) { - if (!isDeeplyEqual(layout, currentPersisted)) { - set(pageLayoutPersistedState, layout); - set(pageLayoutDraftState, { - name: layout.name, - type: layout.type, - objectMetadataId: layout.objectMetadataId, - tabs: layout.tabs, - }); - - if (layout.tabs.length > 0) { - const tabLayouts: TabLayouts = {}; - layout.tabs.forEach((tab) => { - const layouts = tab.widgets.map((w) => ({ - i: w.id, - x: w.gridPosition.column, - y: w.gridPosition.row, - w: w.gridPosition.columnSpan, - h: w.gridPosition.rowSpan, - })); - tabLayouts[tab.id] = { - desktop: layouts, - mobile: layouts.map((l) => ({ ...l, w: 1, x: 0 })), - }; - }); - set(pageLayoutCurrentLayoutsState, tabLayouts); - } else { - set(pageLayoutCurrentLayoutsState, {}); - } - } - } else { - const defaultTab = { - id: `tab-${uuidv4()}`, - title: 'Main', - position: 0, - pageLayoutId: '', - widgets: [], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - deletedAt: null, - }; - - set(pageLayoutDraftState, { - name: '', - type: PageLayoutType.DASHBOARD, - objectMetadataId: null, - tabs: [defaultTab], - }); - set(pageLayoutPersistedState, undefined); - set(pageLayoutCurrentLayoutsState, { - [defaultTab.id]: { desktop: [], mobile: [] }, - }); - } - }, - [], - ); - - useEffect(() => { - if (!isInitialized) { - const existingLayout = isEditMode - ? savedPageLayouts.find((l) => l.id === layoutId) - : undefined; - - const layoutToInitialize = existingLayout || pageLayout; - - initializePageLayout(layoutToInitialize); - setIsInitialized(true); - } - }, [ - layoutId, - savedPageLayouts, - initializePageLayout, - isInitialized, - isEditMode, - pageLayout, - ]); - - return null; -}; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx new file mode 100644 index 0000000000..ab06cded0f --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx @@ -0,0 +1,96 @@ +import { FIND_ONE_PAGE_LAYOUT } from '@/dashboards/graphql/queries/findOnePageLayout'; +import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; +import { type PageLayoutWithData } from '@/page-layout/types/pageLayoutTypes'; +import { type TabLayouts } from '@/page-layout/types/tab-layouts'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useQuery } from '@apollo/client'; +import { useEffect, useState } from 'react'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; + +type PageLayoutInitializationQueryEffectProps = { + pageLayoutId: string; +}; + +export const PageLayoutInitializationQueryEffect = ({ + pageLayoutId, +}: PageLayoutInitializationQueryEffectProps) => { + const [isInitialized, setIsInitialized] = useState(false); + + const { data } = useQuery(FIND_ONE_PAGE_LAYOUT, { + variables: { + id: pageLayoutId, + }, + }); + + const pageLayout: PageLayoutWithData | undefined = data?.getPageLayout; + + const pageLayoutPersistedComponentCallbackState = + useRecoilComponentCallbackState(pageLayoutPersistedComponentState); + + const pageLayoutDraftComponentCallbackState = useRecoilComponentCallbackState( + pageLayoutDraftComponentState, + ); + + const pageLayoutCurrentLayoutsComponentCallbackState = + useRecoilComponentCallbackState(pageLayoutCurrentLayoutsComponentState); + + const initializePageLayout = useRecoilCallback( + ({ set, snapshot }) => + (layout: PageLayoutWithData) => { + const currentPersisted = getSnapshotValue( + snapshot, + pageLayoutPersistedComponentCallbackState, + ); + + if (!isDeeplyEqual(layout, currentPersisted)) { + set(pageLayoutPersistedComponentCallbackState, layout); + set(pageLayoutDraftComponentCallbackState, { + id: layout.id, + name: layout.name, + type: layout.type, + objectMetadataId: layout.objectMetadataId, + tabs: layout.tabs, + }); + + if (layout.tabs.length > 0) { + const tabLayouts: TabLayouts = {}; + layout.tabs.forEach((tab) => { + const layouts = tab.widgets.map((w) => ({ + i: w.id, + x: w.gridPosition.column, + y: w.gridPosition.row, + w: w.gridPosition.columnSpan, + h: w.gridPosition.rowSpan, + })); + tabLayouts[tab.id] = { + desktop: layouts, + mobile: layouts.map((l) => ({ ...l, w: 1, x: 0 })), + }; + }); + set(pageLayoutCurrentLayoutsComponentCallbackState, tabLayouts); + } else { + set(pageLayoutCurrentLayoutsComponentCallbackState, {}); + } + } + }, + [ + pageLayoutCurrentLayoutsComponentCallbackState, + pageLayoutDraftComponentCallbackState, + pageLayoutPersistedComponentCallbackState, + ], + ); + + useEffect(() => { + if (!isInitialized && isDefined(pageLayout)) { + initializePageLayout(pageLayout); + setIsInitialized(true); + } + }, [initializePageLayout, isInitialized, pageLayout]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx index ca5dd9f449..53cc48364a 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx @@ -1,156 +1,33 @@ -import { PageLayoutInitializationEffect } from '@/page-layout/components/PageLayoutInitializationEffect'; -import { EMPTY_LAYOUT } from '@/page-layout/constants/EmptyLayout'; -import { - PAGE_LAYOUT_CONFIG, - type PageLayoutBreakpoint, -} from '@/page-layout/constants/PageLayoutBreakpoints'; -import { pageLayoutCurrentBreakpointState } from '@/page-layout/states/pageLayoutCurrentBreakpointState'; -import { pageLayoutCurrentLayoutsState } from '@/page-layout/states/pageLayoutCurrentLayoutsState'; -import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer'; -import { type Widget } from '@/page-layout/widgets/types/Widget'; -import { TabList } from '@/ui/layout/tab-list/components/TabList'; -import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { PageLayoutInitializationQueryEffect } from '@/page-layout/components/PageLayoutInitializationQueryEffect'; + +import { PageLayoutRendererContent } from '@/page-layout/components/PageLayoutRendererContent'; +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId'; import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext'; -import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import styled from '@emotion/styled'; -import { useMemo } from 'react'; -import { - Responsive, - WidthProvider, - type ResponsiveProps, -} from 'react-grid-layout'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; -import { useRecoilState, useRecoilValue } from 'recoil'; -import { type PageLayoutWithData } from '../types/pageLayoutTypes'; - -const StyledGridContainer = styled.div` - background: ${({ theme }) => theme.background.secondary}; - box-sizing: border-box; - flex: 1; - min-height: 100%; - overflow-y: auto; - overflow-x: hidden; - position: relative; - padding: ${({ theme }) => theme.spacing(2)}; - width: 100%; - user-select: none; - - .react-grid-placeholder { - background: ${({ theme }) => theme.adaptiveColors.blue3} !important; - - border-radius: ${({ theme }) => theme.border.radius.sm}; - } - - .react-grid-item:not(.react-draggable-dragging) { - user-select: auto; - } -`; - -const StyledTabList = styled(TabList)` - padding-left: ${({ theme }) => theme.spacing(2)}; -`; - -type ExtendedResponsiveProps = ResponsiveProps & { - maxCols?: number; - preventCollision?: boolean; -}; - -const ResponsiveGridLayout = WidthProvider( - Responsive, -) as React.ComponentType; type PageLayoutRendererProps = { - pageLayout: PageLayoutWithData; + pageLayoutId: string; }; -type PageLayoutRendererContentProps = { - pageLayout: PageLayoutWithData; -}; - -const PageLayoutRendererContent = ({ - pageLayout, -}: PageLayoutRendererContentProps) => { - const [, setPageLayoutCurrentBreakpoint] = useRecoilState( - pageLayoutCurrentBreakpointState, - ); - - const pageLayoutCurrentLayouts = useRecoilValue( - pageLayoutCurrentLayoutsState, - ); - - const activeTabId = useRecoilComponentValue(activeTabIdComponentState); - - const activeTabWidgets = pageLayout.tabs.find( - (tab) => tab.id === activeTabId, - )?.widgets; - - const tabListTabs: SingleTabProps[] = useMemo(() => { - return [...pageLayout.tabs] - .sort((a, b) => a.position - b.position) - .map((tab) => ({ - id: tab.id, - title: tab.title, - })); - }, [pageLayout.tabs]); - +export const PageLayoutRenderer = ({ + pageLayoutId, +}: PageLayoutRendererProps) => { return ( - <> - - {pageLayout.tabs.length > 0 && ( - - )} - - - setPageLayoutCurrentBreakpoint( - newBreakpoint as PageLayoutBreakpoint, - ) - } - > - {activeTabWidgets?.map((widget) => ( -
- -
- ))} -
-
- - ); -}; - -export const PageLayoutRenderer = ({ pageLayout }: PageLayoutRendererProps) => { - return ( - - - + + + + + ); }; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx new file mode 100644 index 0000000000..fdf258d8b2 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx @@ -0,0 +1,31 @@ +import { PageLayoutGridLayout } from '@/page-layout/components/PageLayoutGridLayout'; +import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout'; +import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId'; +import { TabList } from '@/ui/layout/tab-list/components/TabList'; +import styled from '@emotion/styled'; +import { isDefined } from 'twenty-shared/utils'; + +const StyledTabList = styled(TabList)` + padding-left: ${({ theme }) => theme.spacing(2)}; +`; + +export const PageLayoutRendererContent = () => { + const { currentPageLayout } = useCurrentPageLayout(); + + if (!isDefined(currentPageLayout)) { + return null; + } + + return ( + <> + + + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx index e96cb776a0..97e4ce8747 100644 --- a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx @@ -1,11 +1,11 @@ +import { MockedProvider, type MockedResponse } from '@apollo/client/testing'; import type { Meta, StoryObj } from '@storybook/react'; -import { expect, waitFor, within } from '@storybook/test'; +import { expect, within } from '@storybook/test'; import { MemoryRouter } from 'react-router-dom'; +import { FIND_ONE_PAGE_LAYOUT } from '@/dashboards/graphql/queries/findOnePageLayout'; import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer'; import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets'; -import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; -import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext'; import { RecoilRoot } from 'recoil'; import { PageLayoutType } from '~/generated/graphql'; import { type PageLayoutWidgetWithData } from '../../types/pageLayoutTypes'; @@ -13,24 +13,14 @@ import { type PageLayoutWidgetWithData } from '../../types/pageLayoutTypes'; const validatePageLayoutContent = async (canvasElement: HTMLElement) => { const canvas = within(canvasElement); - await waitFor(async () => { - const revenueElements = canvas.getAllByText('Revenue'); - expect(revenueElements).toHaveLength(2); - - const goalProgressElements = canvas.getAllByText('Goal Progress'); - expect(goalProgressElements).toHaveLength(2); - - expect(canvas.getByText('Product Sales')).toBeInTheDocument(); - expect(canvas.getByText('Services')).toBeInTheDocument(); - expect(canvas.getByText('Support')).toBeInTheDocument(); - }); - + await expect(await canvas.findByText('Revenue')).toBeVisible(); + await expect(await canvas.findByText('Goal Progress')).toBeVisible(); await expect(await canvas.findByText('Revenue Sources')).toBeVisible(); await expect(await canvas.findByText('Quarterly Comparison')).toBeVisible(); - await expect(await canvas.findByText('$125,000')).toBeVisible(); }; -const mixedGraphsPageLayout = { +const mixedGraphsPageLayoutMocks = { + __typename: 'PageLayout', id: 'mixed-graphs-layout', name: 'Mixed Graph Dashboard', type: PageLayoutType.DASHBOARD, @@ -40,6 +30,7 @@ const mixedGraphsPageLayout = { deletedAt: null, tabs: [ { + __typename: 'PageLayoutTab', id: 'mixed-tab', title: 'Mixed Graphs', position: 0, @@ -49,12 +40,14 @@ const mixedGraphsPageLayout = { deletedAt: null, widgets: [ { + __typename: 'PageLayoutWidget', id: 'number-widget', pageLayoutTabId: 'mixed-tab', type: WidgetType.GRAPH, title: 'Revenue', objectMetadataId: null, gridPosition: { + __typename: 'GridPosition', row: 0, column: 0, rowSpan: 2, @@ -72,12 +65,14 @@ const mixedGraphsPageLayout = { deletedAt: null, } as PageLayoutWidgetWithData, { + __typename: 'PageLayoutWidget', id: 'gauge-widget', pageLayoutTabId: 'mixed-tab', type: WidgetType.GRAPH, title: 'Goal Progress', objectMetadataId: null, gridPosition: { + __typename: 'GridPosition', row: 0, column: 3, rowSpan: 4, @@ -97,12 +92,14 @@ const mixedGraphsPageLayout = { deletedAt: null, } as PageLayoutWidgetWithData, { + __typename: 'PageLayoutWidget', id: 'pie-widget', pageLayoutTabId: 'mixed-tab', type: WidgetType.GRAPH, title: 'Revenue Sources', objectMetadataId: null, gridPosition: { + __typename: 'GridPosition', row: 0, column: 6, rowSpan: 4, @@ -123,12 +120,14 @@ const mixedGraphsPageLayout = { deletedAt: null, } as PageLayoutWidgetWithData, { + __typename: 'PageLayoutWidget', id: 'bar-widget', pageLayoutTabId: 'mixed-tab', type: WidgetType.GRAPH, title: 'Quarterly Comparison', objectMetadataId: null, gridPosition: { + __typename: 'GridPosition', row: 2, column: 0, rowSpan: 4, @@ -161,28 +160,33 @@ const mixedGraphsPageLayout = { ], }; +const graphqlMocks: MockedResponse[] = [ + { + request: { + query: FIND_ONE_PAGE_LAYOUT, + variables: { + id: 'mixed-graphs-layout', + }, + }, + result: { + data: { + getPageLayout: mixedGraphsPageLayoutMocks, + }, + }, + }, +]; + const meta: Meta = { title: 'Modules/PageLayout/PageLayoutRenderer', component: PageLayoutRenderer, decorators: [ - (Story, { args }: { args: any }) => ( + (Story) => ( - { - set( - activeTabIdComponentState.atomFamily({ - instanceId: 'page-layout-stories', - }), - args.activeTabId, - ); - }} - > - + + - - + + ), ], @@ -190,7 +194,7 @@ const meta: Meta = { layout: 'fullscreen', }, args: { - pageLayout: mixedGraphsPageLayout, + pageLayoutId: mixedGraphsPageLayoutMocks.id, }, }; @@ -199,9 +203,6 @@ export default meta; type Story = StoryObj; export const DesktopView: Story = { - args: { - activeTabId: 'mixed-tab', - }, parameters: { viewport: { defaultViewport: 'desktop1', @@ -213,9 +214,6 @@ export const DesktopView: Story = { }; export const MobileView: Story = { - args: { - activeTabId: 'mixed-tab', - }, parameters: { viewport: { defaultViewport: 'mobile1', diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/PageLayoutTestWrapper.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/PageLayoutTestWrapper.tsx new file mode 100644 index 0000000000..1fdeb04083 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/PageLayoutTestWrapper.tsx @@ -0,0 +1,32 @@ +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId'; +import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext'; +import { type ReactNode } from 'react'; +import { RecoilRoot, type MutableSnapshot } from 'recoil'; + +export const PAGE_LAYOUT_TEST_INSTANCE_ID = + '20202020-f244-4ae0-906b-78958aa07642'; + +export const PageLayoutTestWrapper = ({ + children, + initializeState, + instanceId: instanceIdFromProps, +}: { + children: ReactNode; + initializeState?: (snapshot: MutableSnapshot) => void; + instanceId?: string; +}) => { + const instanceId = instanceIdFromProps ?? PAGE_LAYOUT_TEST_INSTANCE_ID; + + return ( + + + {children} + + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useChangePageLayoutDragSelection.test.ts b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useChangePageLayoutDragSelection.test.ts deleted file mode 100644 index 3914c73aed..0000000000 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useChangePageLayoutDragSelection.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { act, renderHook } from '@testing-library/react'; -import { type ReactNode } from 'react'; -import { RecoilRoot, useRecoilValue } from 'recoil'; -import { pageLayoutSelectedCellsState } from '../../states/pageLayoutSelectedCellsState'; -import { useChangePageLayoutDragSelection } from '../useChangePageLayoutDragSelection'; - -describe('useChangePageLayoutDragSelection', () => { - it('should add cell to selection when selected is true', () => { - const { result } = renderHook( - () => ({ - changeDragSelection: useChangePageLayoutDragSelection(), - selectedCells: useRecoilValue(pageLayoutSelectedCellsState), - }), - { - wrapper: ({ children }: { children: ReactNode }) => - RecoilRoot({ - initializeState: ({ set }) => { - set(pageLayoutSelectedCellsState, new Set(['cell-1'])); - }, - children, - }), - }, - ); - - expect(result.current.selectedCells.size).toBe(1); - expect(result.current.selectedCells.has('cell-2')).toBe(false); - - act(() => { - result.current.changeDragSelection.changePageLayoutDragSelection( - 'cell-2', - true, - ); - }); - - expect(result.current.selectedCells.size).toBe(2); - expect(result.current.selectedCells.has('cell-1')).toBe(true); - expect(result.current.selectedCells.has('cell-2')).toBe(true); - }); - - it('should remove cell from selection when selected is false', () => { - const { result } = renderHook( - () => ({ - changeDragSelection: useChangePageLayoutDragSelection(), - selectedCells: useRecoilValue(pageLayoutSelectedCellsState), - }), - { - wrapper: ({ children }: { children: ReactNode }) => - RecoilRoot({ - initializeState: ({ set }) => { - set(pageLayoutSelectedCellsState, new Set(['cell-1', 'cell-2'])); - }, - children, - }), - }, - ); - - expect(result.current.selectedCells.size).toBe(2); - expect(result.current.selectedCells.has('cell-2')).toBe(true); - - act(() => { - result.current.changeDragSelection.changePageLayoutDragSelection( - 'cell-2', - false, - ); - }); - - expect(result.current.selectedCells.size).toBe(1); - expect(result.current.selectedCells.has('cell-1')).toBe(true); - expect(result.current.selectedCells.has('cell-2')).toBe(false); - }); - - it('should handle adding same cell multiple times', () => { - const { result } = renderHook( - () => ({ - changeDragSelection: useChangePageLayoutDragSelection(), - selectedCells: useRecoilValue(pageLayoutSelectedCellsState), - }), - { - wrapper: RecoilRoot, - }, - ); - - act(() => { - result.current.changeDragSelection.changePageLayoutDragSelection( - 'cell-1', - true, - ); - }); - expect(result.current.selectedCells.size).toBe(1); - - act(() => { - result.current.changeDragSelection.changePageLayoutDragSelection( - 'cell-1', - true, - ); - }); - expect(result.current.selectedCells.size).toBe(1); - }); - - it('should handle removing non-existent cell', () => { - const { result } = renderHook( - () => ({ - changeDragSelection: useChangePageLayoutDragSelection(), - selectedCells: useRecoilValue(pageLayoutSelectedCellsState), - }), - { - wrapper: ({ children }: { children: ReactNode }) => - RecoilRoot({ - initializeState: ({ set }) => { - set(pageLayoutSelectedCellsState, new Set(['cell-1'])); - }, - children, - }), - }, - ); - - expect(result.current.selectedCells.size).toBe(1); - - act(() => { - result.current.changeDragSelection.changePageLayoutDragSelection( - 'cell-99', - false, - ); - }); - - expect(result.current.selectedCells.size).toBe(1); - expect(result.current.selectedCells.has('cell-1')).toBe(true); - }); - - it('should return a function', () => { - const { result } = renderHook(() => useChangePageLayoutDragSelection(), { - wrapper: RecoilRoot, - }); - - expect(typeof result.current.changePageLayoutDragSelection).toBe( - 'function', - ); - }); -}); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useChangePageLayoutDragSelection.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useChangePageLayoutDragSelection.test.tsx new file mode 100644 index 0000000000..a454ecca73 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useChangePageLayoutDragSelection.test.tsx @@ -0,0 +1,191 @@ +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { act, renderHook } from '@testing-library/react'; +import { type ReactNode } from 'react'; +import { pageLayoutSelectedCellsComponentState } from '../../states/pageLayoutSelectedCellsComponentState'; +import { + PageLayoutTestWrapper, + PAGE_LAYOUT_TEST_INSTANCE_ID, +} from './PageLayoutTestWrapper'; +import { useChangePageLayoutDragSelection } from '../useChangePageLayoutDragSelection'; + +describe('useChangePageLayoutDragSelection', () => { + it('should add cell to selection when selected is true', () => { + const { result } = renderHook( + () => ({ + changeDragSelection: useChangePageLayoutDragSelection( + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + selectedCells: useRecoilComponentValue( + pageLayoutSelectedCellsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }: { children: ReactNode }) => ( + { + set( + pageLayoutSelectedCellsComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + new Set(['cell-1']), + ); + }} + > + {children} + + ), + }, + ); + + expect(result.current.selectedCells.size).toBe(1); + expect(result.current.selectedCells.has('cell-2')).toBe(false); + + act(() => { + result.current.changeDragSelection.changePageLayoutDragSelection( + 'cell-2', + true, + ); + }); + + expect(result.current.selectedCells.size).toBe(2); + expect(result.current.selectedCells.has('cell-1')).toBe(true); + expect(result.current.selectedCells.has('cell-2')).toBe(true); + }); + + it('should remove cell from selection when selected is false', () => { + const { result } = renderHook( + () => ({ + changeDragSelection: useChangePageLayoutDragSelection( + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + selectedCells: useRecoilComponentValue( + pageLayoutSelectedCellsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }: { children: ReactNode }) => ( + { + set( + pageLayoutSelectedCellsComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + new Set(['cell-1', 'cell-2']), + ); + }} + > + {children} + + ), + }, + ); + + expect(result.current.selectedCells.size).toBe(2); + expect(result.current.selectedCells.has('cell-2')).toBe(true); + + act(() => { + result.current.changeDragSelection.changePageLayoutDragSelection( + 'cell-2', + false, + ); + }); + + expect(result.current.selectedCells.size).toBe(1); + expect(result.current.selectedCells.has('cell-1')).toBe(true); + expect(result.current.selectedCells.has('cell-2')).toBe(false); + }); + + it('should handle adding same cell multiple times', () => { + const { result } = renderHook( + () => ({ + changeDragSelection: useChangePageLayoutDragSelection( + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + selectedCells: useRecoilComponentValue( + pageLayoutSelectedCellsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }, + ); + + act(() => { + result.current.changeDragSelection.changePageLayoutDragSelection( + 'cell-1', + true, + ); + }); + expect(result.current.selectedCells.size).toBe(1); + + act(() => { + result.current.changeDragSelection.changePageLayoutDragSelection( + 'cell-1', + true, + ); + }); + expect(result.current.selectedCells.size).toBe(1); + }); + + it('should handle removing non-existent cell', () => { + const { result } = renderHook( + () => ({ + changeDragSelection: useChangePageLayoutDragSelection( + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + selectedCells: useRecoilComponentValue( + pageLayoutSelectedCellsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }: { children: ReactNode }) => ( + { + set( + pageLayoutSelectedCellsComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + new Set(['cell-1']), + ); + }} + > + {children} + + ), + }, + ); + + expect(result.current.selectedCells.size).toBe(1); + + act(() => { + result.current.changeDragSelection.changePageLayoutDragSelection( + 'cell-99', + false, + ); + }); + + expect(result.current.selectedCells.size).toBe(1); + expect(result.current.selectedCells.has('cell-1')).toBe(true); + }); + + it('should return a function', () => { + const { result } = renderHook( + () => useChangePageLayoutDragSelection(PAGE_LAYOUT_TEST_INSTANCE_ID), + { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }, + ); + + expect(typeof result.current.changePageLayoutDragSelection).toBe( + 'function', + ); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutTab.test.ts b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutTab.test.tsx similarity index 65% rename from packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutTab.test.ts rename to packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutTab.test.tsx index 39b567b74c..3d61a3e43c 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutTab.test.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutTab.test.tsx @@ -1,8 +1,12 @@ -import { pageLayoutCurrentLayoutsState } from '@/page-layout/states/pageLayoutCurrentLayoutsState'; -import { pageLayoutDraftState } from '@/page-layout/states/pageLayoutDraftState'; +import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot, useRecoilValue } from 'recoil'; import { useCreatePageLayoutTab } from '../useCreatePageLayoutTab'; +import { + PageLayoutTestWrapper, + PAGE_LAYOUT_TEST_INSTANCE_ID, +} from './PageLayoutTestWrapper'; jest.mock('uuid', () => ({ v4: jest.fn(), @@ -18,12 +22,18 @@ describe('useCreatePageLayoutTab', () => { uuidModule.v4.mockReturnValue('mock-uuid'); const { result } = renderHook( () => ({ - createTab: useCreatePageLayoutTab(), - pageLayoutCurrentLayouts: useRecoilValue(pageLayoutCurrentLayoutsState), - pageLayoutDraft: useRecoilValue(pageLayoutDraftState), + createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID), + pageLayoutCurrentLayouts: useRecoilComponentValue( + pageLayoutCurrentLayoutsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + pageLayoutDraft: useRecoilComponentValue( + pageLayoutDraftComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), }), { - wrapper: RecoilRoot, + wrapper: PageLayoutTestWrapper, }, ); @@ -50,11 +60,14 @@ describe('useCreatePageLayoutTab', () => { uuidModule.v4.mockReturnValue('mock-uuid'); const { result } = renderHook( () => ({ - createTab: useCreatePageLayoutTab(), - pageLayoutDraft: useRecoilValue(pageLayoutDraftState), + createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID), + pageLayoutDraft: useRecoilComponentValue( + pageLayoutDraftComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), }), { - wrapper: RecoilRoot, + wrapper: PageLayoutTestWrapper, }, ); @@ -74,11 +87,14 @@ describe('useCreatePageLayoutTab', () => { .mockReturnValueOnce('mock-uuid-2'); const { result } = renderHook( () => ({ - createTab: useCreatePageLayoutTab(), - pageLayoutDraft: useRecoilValue(pageLayoutDraftState), + createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID), + pageLayoutDraft: useRecoilComponentValue( + pageLayoutDraftComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), }), { - wrapper: RecoilRoot, + wrapper: PageLayoutTestWrapper, }, ); @@ -104,11 +120,14 @@ describe('useCreatePageLayoutTab', () => { .mockReturnValueOnce('mock-uuid-2'); const { result } = renderHook( () => ({ - createTab: useCreatePageLayoutTab(), - pageLayoutCurrentLayouts: useRecoilValue(pageLayoutCurrentLayoutsState), + createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID), + pageLayoutCurrentLayouts: useRecoilComponentValue( + pageLayoutCurrentLayoutsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), }), { - wrapper: RecoilRoot, + wrapper: PageLayoutTestWrapper, }, ); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutWidget.test.ts b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutWidget.test.tsx similarity index 66% rename from packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutWidget.test.ts rename to packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutWidget.test.tsx index d8f289a575..5d74243aae 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutWidget.test.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutWidget.test.tsx @@ -1,12 +1,17 @@ -import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '@/page-layout/constants/SettingsPageLayoutTabsInstanceId'; import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets'; -import { pageLayoutCurrentLayoutsState } from '@/page-layout/states/pageLayoutCurrentLayoutsState'; -import { pageLayoutDraftState } from '@/page-layout/states/pageLayoutDraftState'; +import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useSetRecoilState } from 'recoil'; import { PageLayoutType } from '~/generated/graphql'; import { useCreatePageLayoutWidget } from '../useCreatePageLayoutWidget'; +import { + PAGE_LAYOUT_TEST_INSTANCE_ID, + PageLayoutTestWrapper, +} from './PageLayoutTestWrapper'; jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid'), @@ -22,16 +27,25 @@ describe('useCreatePageLayoutWidget', () => { () => { const setActiveTabId = useSetRecoilState( activeTabIdComponentState.atomFamily({ - instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID, + instanceId: `${PAGE_LAYOUT_TEST_INSTANCE_ID}-tab-list`, }), ); - const setPageLayoutDraft = useSetRecoilState(pageLayoutDraftState); - const pageLayoutDraft = useRecoilValue(pageLayoutDraftState); - const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets); - const pageLayoutCurrentLayouts = useRecoilValue( - pageLayoutCurrentLayoutsState, + const setPageLayoutDraft = useSetRecoilComponentState( + pageLayoutDraftComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ); + const pageLayoutDraft = useRecoilComponentValue( + pageLayoutDraftComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ); + const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets); + const pageLayoutCurrentLayouts = useRecoilComponentValue( + pageLayoutCurrentLayoutsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ); + const createWidget = useCreatePageLayoutWidget( + PAGE_LAYOUT_TEST_INSTANCE_ID, ); - const createWidget = useCreatePageLayoutWidget(); return { setActiveTabId, setPageLayoutDraft, @@ -41,12 +55,13 @@ describe('useCreatePageLayoutWidget', () => { }; }, { - wrapper: RecoilRoot, + wrapper: PageLayoutTestWrapper, }, ); act(() => { result.current.setPageLayoutDraft({ + id: 'test-layout', name: 'Test Layout', type: PageLayoutType.DASHBOARD, objectMetadataId: null, @@ -86,18 +101,27 @@ describe('useCreatePageLayoutWidget', () => { it('should handle different graph types', () => { const { result } = renderHook( () => { - const setPageLayoutDraft = useSetRecoilState(pageLayoutDraftState); + const setPageLayoutDraft = useSetRecoilComponentState( + pageLayoutDraftComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ); const setActiveTabId = useSetRecoilState( activeTabIdComponentState.atomFamily({ - instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID, + instanceId: `${PAGE_LAYOUT_TEST_INSTANCE_ID}-tab-list`, }), ); - const pageLayoutDraft = useRecoilValue(pageLayoutDraftState); - const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets); - const pageLayoutCurrentLayouts = useRecoilValue( - pageLayoutCurrentLayoutsState, + const pageLayoutDraft = useRecoilComponentValue( + pageLayoutDraftComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ); + const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets); + const pageLayoutCurrentLayouts = useRecoilComponentValue( + pageLayoutCurrentLayoutsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ); + const createWidget = useCreatePageLayoutWidget( + PAGE_LAYOUT_TEST_INSTANCE_ID, ); - const createWidget = useCreatePageLayoutWidget(); return { setPageLayoutDraft, setActiveTabId, @@ -108,12 +132,13 @@ describe('useCreatePageLayoutWidget', () => { }; }, { - wrapper: RecoilRoot, + wrapper: PageLayoutTestWrapper, }, ); act(() => { result.current.setPageLayoutDraft({ + id: 'test-layout', name: 'Test Layout', type: PageLayoutType.DASHBOARD, objectMetadataId: null, @@ -173,16 +198,22 @@ describe('useCreatePageLayoutWidget', () => { it('should not create widget when activeTabId is null', () => { const { result } = renderHook( () => { - const pageLayoutDraft = useRecoilValue(pageLayoutDraftState); - const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets); - const pageLayoutCurrentLayouts = useRecoilValue( - pageLayoutCurrentLayoutsState, + const pageLayoutDraft = useRecoilComponentValue( + pageLayoutDraftComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ); + const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets); + const pageLayoutCurrentLayouts = useRecoilComponentValue( + pageLayoutCurrentLayoutsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ); + const createWidget = useCreatePageLayoutWidget( + PAGE_LAYOUT_TEST_INSTANCE_ID, ); - const createWidget = useCreatePageLayoutWidget(); return { allWidgets, pageLayoutCurrentLayouts, createWidget }; }, { - wrapper: RecoilRoot, + wrapper: PageLayoutTestWrapper, }, ); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.ts b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.tsx similarity index 58% rename from packages/twenty-front/src/modules/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.ts rename to packages/twenty-front/src/modules/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.tsx index 0873d5f112..0365af6599 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.tsx @@ -1,12 +1,18 @@ import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; +import { + PageLayoutTestWrapper, + PAGE_LAYOUT_TEST_INSTANCE_ID, +} from './PageLayoutTestWrapper'; import { useDeletePageLayoutWidget } from '../useDeletePageLayoutWidget'; describe('useDeletePageLayoutWidget', () => { it('should remove widget from all states', () => { - const { result } = renderHook(() => useDeletePageLayoutWidget(), { - wrapper: RecoilRoot, - }); + const { result } = renderHook( + () => useDeletePageLayoutWidget(PAGE_LAYOUT_TEST_INSTANCE_ID), + { + wrapper: PageLayoutTestWrapper, + }, + ); act(() => { result.current.deletePageLayoutWidget('widget-1'); @@ -16,9 +22,12 @@ describe('useDeletePageLayoutWidget', () => { }); it('should handle removing non-existent widget', () => { - const { result } = renderHook(() => useDeletePageLayoutWidget(), { - wrapper: RecoilRoot, - }); + const { result } = renderHook( + () => useDeletePageLayoutWidget(PAGE_LAYOUT_TEST_INSTANCE_ID), + { + wrapper: PageLayoutTestWrapper, + }, + ); act(() => { result.current.deletePageLayoutWidget('non-existent-widget'); @@ -28,9 +37,12 @@ describe('useDeletePageLayoutWidget', () => { }); it('should handle empty layouts', () => { - const { result } = renderHook(() => useDeletePageLayoutWidget(), { - wrapper: RecoilRoot, - }); + const { result } = renderHook( + () => useDeletePageLayoutWidget(PAGE_LAYOUT_TEST_INSTANCE_ID), + { + wrapper: PageLayoutTestWrapper, + }, + ); act(() => { result.current.deletePageLayoutWidget('any-widget'); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useEndPageLayoutDragSelection.test.ts b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useEndPageLayoutDragSelection.test.ts deleted file mode 100644 index 7d24cfd121..0000000000 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useEndPageLayoutDragSelection.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; -import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; -import { act, renderHook } from '@testing-library/react'; -import { type ReactNode } from 'react'; -import { RecoilRoot, useRecoilValue } from 'recoil'; -import { IconAppWindow } from 'twenty-ui/display'; -import { pageLayoutDraggedAreaState } from '../../states/pageLayoutDraggedAreaState'; -import { pageLayoutSelectedCellsState } from '../../states/pageLayoutSelectedCellsState'; -import { calculateGridBoundsFromSelectedCells } from '../../utils/calculateGridBoundsFromSelectedCells'; -import { useEndPageLayoutDragSelection } from '../useEndPageLayoutDragSelection'; - -jest.mock('@/command-menu/hooks/useNavigateCommandMenu'); -jest.mock('../../utils/calculateGridBoundsFromSelectedCells'); - -describe('useEndPageLayoutDragSelection', () => { - const mockNavigateCommandMenu = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - (useNavigateCommandMenu as jest.Mock).mockReturnValue({ - navigateCommandMenu: mockNavigateCommandMenu, - }); - }); - - it('should handle drag selection end with valid bounds', () => { - const mockBounds = { x: 0, y: 0, w: 2, h: 2 }; - (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue( - mockBounds, - ); - - const { result } = renderHook( - () => ({ - endDragSelection: useEndPageLayoutDragSelection(), - selectedCells: useRecoilValue(pageLayoutSelectedCellsState), - draggedArea: useRecoilValue(pageLayoutDraggedAreaState), - }), - { - wrapper: ({ children }: { children: ReactNode }) => - RecoilRoot({ - initializeState: ({ set }) => { - set( - pageLayoutSelectedCellsState, - new Set(['0-0', '0-1', '1-0', '1-1']), - ); - set(pageLayoutDraggedAreaState, null); - }, - children, - }), - }, - ); - - expect(result.current.selectedCells.size).toBe(4); - expect(result.current.draggedArea).toBeNull(); - - act(() => { - result.current.endDragSelection.endPageLayoutDragSelection(); - }); - - expect(calculateGridBoundsFromSelectedCells).toHaveBeenCalledWith([ - '0-0', - '0-1', - '1-0', - '1-1', - ]); - - expect(result.current.draggedArea).toEqual(mockBounds); - expect(result.current.selectedCells.size).toBe(0); - - expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ - page: CommandMenuPages.PageLayoutWidgetTypeSelect, - pageTitle: 'Add Widget', - pageIcon: IconAppWindow, - resetNavigationStack: true, - }); - }); - - it('should not navigate when no cells are selected', () => { - (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue(null); - - const { result } = renderHook( - () => ({ - endDragSelection: useEndPageLayoutDragSelection(), - selectedCells: useRecoilValue(pageLayoutSelectedCellsState), - draggedArea: useRecoilValue(pageLayoutDraggedAreaState), - }), - { - wrapper: ({ children }: { children: ReactNode }) => - RecoilRoot({ - initializeState: ({ set }) => { - set(pageLayoutSelectedCellsState, new Set()); - set(pageLayoutDraggedAreaState, null); - }, - children, - }), - }, - ); - - act(() => { - result.current.endDragSelection.endPageLayoutDragSelection(); - }); - - expect(calculateGridBoundsFromSelectedCells).not.toHaveBeenCalled(); - expect(mockNavigateCommandMenu).not.toHaveBeenCalled(); - expect(result.current.draggedArea).toBeNull(); - expect(result.current.selectedCells.size).toBe(0); - }); - - it('should not navigate when bounds calculation returns null', () => { - (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue(null); - - const { result } = renderHook( - () => ({ - endDragSelection: useEndPageLayoutDragSelection(), - selectedCells: useRecoilValue(pageLayoutSelectedCellsState), - draggedArea: useRecoilValue(pageLayoutDraggedAreaState), - }), - { - wrapper: ({ children }: { children: ReactNode }) => - RecoilRoot({ - initializeState: ({ set }) => { - set(pageLayoutSelectedCellsState, new Set(['invalid-cell'])); - set(pageLayoutDraggedAreaState, null); - }, - children, - }), - }, - ); - - act(() => { - result.current.endDragSelection.endPageLayoutDragSelection(); - }); - - expect(calculateGridBoundsFromSelectedCells).toHaveBeenCalledWith([ - 'invalid-cell', - ]); - expect(mockNavigateCommandMenu).not.toHaveBeenCalled(); - expect(result.current.draggedArea).toBeNull(); - expect(result.current.selectedCells.size).toBe(1); - }); - - it('should return a function', () => { - const { result } = renderHook(() => useEndPageLayoutDragSelection(), { - wrapper: RecoilRoot, - }); - - expect(typeof result.current.endPageLayoutDragSelection).toBe('function'); - }); - - it('should navigate to widget selection when bounds are valid', () => { - const mockBounds = { x: 0, y: 0, w: 2, h: 2 }; - (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue( - mockBounds, - ); - - const { result } = renderHook( - () => ({ - endDragSelection: useEndPageLayoutDragSelection(), - selectedCells: useRecoilValue(pageLayoutSelectedCellsState), - }), - { - wrapper: ({ children }: { children: ReactNode }) => - RecoilRoot({ - initializeState: ({ set }) => { - set(pageLayoutSelectedCellsState, new Set(['0-0'])); - }, - children, - }), - }, - ); - - act(() => { - result.current.endDragSelection.endPageLayoutDragSelection(); - }); - - expect(mockNavigateCommandMenu).toHaveBeenCalled(); - }); - - it('should clear selected cells after successful navigation', () => { - const mockBounds = { x: 0, y: 0, w: 1, h: 1 }; - (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue( - mockBounds, - ); - - const { result } = renderHook( - () => ({ - endDragSelection: useEndPageLayoutDragSelection(), - selectedCells: useRecoilValue(pageLayoutSelectedCellsState), - }), - { - wrapper: ({ children }: { children: ReactNode }) => - RecoilRoot({ - initializeState: ({ set }) => { - set(pageLayoutSelectedCellsState, new Set(['0-0'])); - }, - children, - }), - }, - ); - - expect(result.current.selectedCells.size).toBe(1); - - act(() => { - result.current.endDragSelection.endPageLayoutDragSelection(); - }); - - expect(result.current.selectedCells.size).toBe(0); - }); -}); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useEndPageLayoutDragSelection.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useEndPageLayoutDragSelection.test.tsx new file mode 100644 index 0000000000..15898345f9 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useEndPageLayoutDragSelection.test.tsx @@ -0,0 +1,271 @@ +import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { act, renderHook } from '@testing-library/react'; +import { type ReactNode } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { pageLayoutDraggedAreaComponentState } from '../../states/pageLayoutDraggedAreaComponentState'; +import { pageLayoutSelectedCellsComponentState } from '../../states/pageLayoutSelectedCellsComponentState'; +import { calculateGridBoundsFromSelectedCells } from '../../utils/calculateGridBoundsFromSelectedCells'; +import { useEndPageLayoutDragSelection } from '../useEndPageLayoutDragSelection'; +import { + PAGE_LAYOUT_TEST_INSTANCE_ID, + PageLayoutTestWrapper, +} from './PageLayoutTestWrapper'; + +jest.mock( + '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu', +); +jest.mock('../../utils/calculateGridBoundsFromSelectedCells'); + +const createInitializeState = + (initialSelectedCells?: Set, initialDraggedArea?: any) => + ({ set }: { set: any }) => { + if (isDefined(initialSelectedCells)) { + set( + pageLayoutSelectedCellsComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + initialSelectedCells, + ); + } + if (initialDraggedArea !== undefined) { + set( + pageLayoutDraggedAreaComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + initialDraggedArea, + ); + } + }; + +describe('useEndPageLayoutDragSelection', () => { + const mockNavigatePageLayoutCommandMenu = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (useNavigatePageLayoutCommandMenu as jest.Mock).mockReturnValue({ + navigatePageLayoutCommandMenu: mockNavigatePageLayoutCommandMenu, + }); + }); + + it('should handle drag selection end with valid bounds', () => { + const mockBounds = { x: 0, y: 0, w: 2, h: 2 }; + (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue( + mockBounds, + ); + + const { result } = renderHook( + () => ({ + endDragSelection: useEndPageLayoutDragSelection( + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + selectedCells: useRecoilComponentValue( + pageLayoutSelectedCellsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + draggedArea: useRecoilComponentValue( + pageLayoutDraggedAreaComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }, + ); + + expect(result.current.selectedCells.size).toBe(4); + expect(result.current.draggedArea).toBeNull(); + + act(() => { + result.current.endDragSelection.endPageLayoutDragSelection(); + }); + + expect(calculateGridBoundsFromSelectedCells).toHaveBeenCalledWith([ + '0-0', + '0-1', + '1-0', + '1-1', + ]); + + expect(result.current.draggedArea).toEqual(mockBounds); + expect(result.current.selectedCells.size).toBe(0); + + expect(mockNavigatePageLayoutCommandMenu).toHaveBeenCalledWith({ + commandMenuPage: CommandMenuPages.PageLayoutWidgetTypeSelect, + }); + }); + + it('should not navigate when no cells are selected', () => { + (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue(null); + + const { result } = renderHook( + () => ({ + endDragSelection: useEndPageLayoutDragSelection( + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + selectedCells: useRecoilComponentValue( + pageLayoutSelectedCellsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + draggedArea: useRecoilComponentValue( + pageLayoutDraggedAreaComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }, + ); + + act(() => { + result.current.endDragSelection.endPageLayoutDragSelection(); + }); + + expect(calculateGridBoundsFromSelectedCells).not.toHaveBeenCalled(); + expect(mockNavigatePageLayoutCommandMenu).not.toHaveBeenCalled(); + expect(result.current.draggedArea).toBeNull(); + expect(result.current.selectedCells.size).toBe(0); + }); + + it('should not navigate when bounds calculation returns null', () => { + (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue(null); + + const { result } = renderHook( + () => ({ + endDragSelection: useEndPageLayoutDragSelection( + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + selectedCells: useRecoilComponentValue( + pageLayoutSelectedCellsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + draggedArea: useRecoilComponentValue( + pageLayoutDraggedAreaComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }, + ); + + act(() => { + result.current.endDragSelection.endPageLayoutDragSelection(); + }); + + expect(calculateGridBoundsFromSelectedCells).toHaveBeenCalledWith([ + 'invalid-cell', + ]); + expect(mockNavigatePageLayoutCommandMenu).not.toHaveBeenCalled(); + expect(result.current.draggedArea).toBeNull(); + expect(result.current.selectedCells.size).toBe(0); + }); + + it('should return a function', () => { + const { result } = renderHook( + () => useEndPageLayoutDragSelection(PAGE_LAYOUT_TEST_INSTANCE_ID), + { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }, + ); + + expect(typeof result.current.endPageLayoutDragSelection).toBe('function'); + }); + + it('should navigate to widget selection when bounds are valid', () => { + const mockBounds = { x: 0, y: 0, w: 2, h: 2 }; + (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue( + mockBounds, + ); + + const { result } = renderHook( + () => ({ + endDragSelection: useEndPageLayoutDragSelection( + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + selectedCells: useRecoilComponentValue( + pageLayoutSelectedCellsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }, + ); + + act(() => { + result.current.endDragSelection.endPageLayoutDragSelection(); + }); + + expect(mockNavigatePageLayoutCommandMenu).toHaveBeenCalled(); + }); + + it('should clear selected cells after successful navigation', () => { + const mockBounds = { x: 0, y: 0, w: 1, h: 1 }; + (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue( + mockBounds, + ); + + const { result } = renderHook( + () => ({ + endDragSelection: useEndPageLayoutDragSelection( + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + selectedCells: useRecoilComponentValue( + pageLayoutSelectedCellsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }, + ); + + expect(result.current.selectedCells.size).toBe(1); + + act(() => { + result.current.endDragSelection.endPageLayoutDragSelection(); + }); + + expect(result.current.selectedCells.size).toBe(0); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.ts b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx similarity index 74% rename from packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.ts rename to packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx index d5cfd74804..1fcf3c0744 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx @@ -1,26 +1,36 @@ import { GraphType, WidgetType } from '@/page-layout/mocks/mockWidgets'; import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; import { PageLayoutType } from '~/generated/graphql'; import { usePageLayoutDraftState } from '../usePageLayoutDraftState'; +import { + PAGE_LAYOUT_TEST_INSTANCE_ID, + PageLayoutTestWrapper, +} from './PageLayoutTestWrapper'; describe('usePageLayoutDraftState', () => { it('should detect dirty state when draft differs from persisted', () => { - const { result } = renderHook(() => usePageLayoutDraftState(), { - wrapper: RecoilRoot, - }); + const { result } = renderHook( + () => usePageLayoutDraftState(PAGE_LAYOUT_TEST_INSTANCE_ID), + { + wrapper: PageLayoutTestWrapper, + }, + ); expect(result.current.isDirty).toBe(false); expect(result.current.canSave).toBe(false); }); it('should handle empty name as not saveable', () => { - const { result } = renderHook(() => usePageLayoutDraftState(), { - wrapper: RecoilRoot, - }); + const { result } = renderHook( + () => usePageLayoutDraftState(PAGE_LAYOUT_TEST_INSTANCE_ID), + { + wrapper: PageLayoutTestWrapper, + }, + ); act(() => { result.current.setPageLayoutDraft({ + id: 'test-layout', name: ' ', type: PageLayoutType.DASHBOARD, objectMetadataId: null, @@ -33,12 +43,16 @@ describe('usePageLayoutDraftState', () => { }); it('should allow updating draft state', () => { - const { result } = renderHook(() => usePageLayoutDraftState(), { - wrapper: RecoilRoot, - }); + const { result } = renderHook( + () => usePageLayoutDraftState(PAGE_LAYOUT_TEST_INSTANCE_ID), + { + wrapper: PageLayoutTestWrapper, + }, + ); act(() => { result.current.setPageLayoutDraft({ + id: 'test-layout', name: 'Updated Name', type: PageLayoutType.DASHBOARD, objectMetadataId: null, @@ -52,12 +66,16 @@ describe('usePageLayoutDraftState', () => { }); it('should detect changes in widgets', () => { - const { result } = renderHook(() => usePageLayoutDraftState(), { - wrapper: RecoilRoot, - }); + const { result } = renderHook( + () => usePageLayoutDraftState(PAGE_LAYOUT_TEST_INSTANCE_ID), + { + wrapper: PageLayoutTestWrapper, + }, + ); act(() => { result.current.setPageLayoutDraft({ + id: 'test-layout', name: 'Test Layout', type: PageLayoutType.DASHBOARD, objectMetadataId: null, diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutHandleLayoutChange.test.ts b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutHandleLayoutChange.test.ts deleted file mode 100644 index 53966ca12d..0000000000 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutHandleLayoutChange.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot, useRecoilValue } from 'recoil'; -import { pageLayoutCurrentLayoutsState } from '../../states/pageLayoutCurrentLayoutsState'; -import { usePageLayoutHandleLayoutChange } from '../usePageLayoutHandleLayoutChange'; - -describe('usePageLayoutHandleLayoutChange', () => { - it('should update layouts for specific tab only', () => { - const { result } = renderHook( - () => ({ - handler: usePageLayoutHandleLayoutChange('tab-1'), - layouts: useRecoilValue(pageLayoutCurrentLayoutsState), - }), - { - wrapper: RecoilRoot, - }, - ); - - const newLayouts = { - desktop: [ - { i: 'widget-1', x: 2, y: 3, w: 4, h: 5 }, - { i: 'widget-2', x: 6, y: 7, w: 8, h: 9 }, - ], - mobile: [ - { i: 'widget-1', x: 0, y: 0, w: 1, h: 5 }, - { i: 'widget-2', x: 0, y: 5, w: 1, h: 9 }, - ], - }; - - act(() => { - result.current.handler.handleLayoutChange([], newLayouts); - }); - - expect(result.current.layouts['tab-1']).toEqual(newLayouts); - expect(result.current.layouts['tab-2']).toBeUndefined(); - }); - - it('should isolate layouts between different tabs', () => { - const { result, rerender } = renderHook( - ({ tabId }) => ({ - handler: usePageLayoutHandleLayoutChange(tabId), - layouts: useRecoilValue(pageLayoutCurrentLayoutsState), - }), - { - wrapper: RecoilRoot, - initialProps: { tabId: 'tab-1' }, - }, - ); - - const tab1Layouts = { - desktop: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2 }], - mobile: [{ i: 'widget-1', x: 0, y: 0, w: 1, h: 2 }], - }; - - act(() => { - result.current.handler.handleLayoutChange([], tab1Layouts); - }); - - rerender({ tabId: 'tab-2' }); - - const tab2Layouts = { - desktop: [{ i: 'widget-2', x: 4, y: 4, w: 3, h: 3 }], - mobile: [{ i: 'widget-2', x: 0, y: 0, w: 1, h: 3 }], - }; - - act(() => { - result.current.handler.handleLayoutChange([], tab2Layouts); - }); - - expect(result.current.layouts['tab-1']).toEqual(tab1Layouts); - expect(result.current.layouts['tab-2']).toEqual(tab2Layouts); - }); - - it('should not update layouts when activeTabId is null', () => { - const { result } = renderHook( - () => ({ - handler: usePageLayoutHandleLayoutChange(null), - layouts: useRecoilValue(pageLayoutCurrentLayoutsState), - }), - { - wrapper: RecoilRoot, - }, - ); - - const newLayouts = { - desktop: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2 }], - mobile: [{ i: 'widget-1', x: 0, y: 0, w: 1, h: 2 }], - }; - - act(() => { - result.current.handler.handleLayoutChange([], newLayouts); - }); - - expect(Object.keys(result.current.layouts)).toHaveLength(0); - }); -}); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutHandleLayoutChange.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutHandleLayoutChange.test.tsx new file mode 100644 index 0000000000..04bf7a8932 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutHandleLayoutChange.test.tsx @@ -0,0 +1,124 @@ +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { act, renderHook } from '@testing-library/react'; +import { pageLayoutCurrentLayoutsComponentState } from '../../states/pageLayoutCurrentLayoutsComponentState'; +import { usePageLayoutHandleLayoutChange } from '../usePageLayoutHandleLayoutChange'; +import { + PAGE_LAYOUT_TEST_INSTANCE_ID, + PageLayoutTestWrapper, +} from './PageLayoutTestWrapper'; + +describe('usePageLayoutHandleLayoutChange', () => { + it('should update layouts for specific tab only', () => { + const { result } = renderHook( + () => ({ + handler: usePageLayoutHandleLayoutChange(PAGE_LAYOUT_TEST_INSTANCE_ID), + layouts: useRecoilComponentValue( + pageLayoutCurrentLayoutsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }) => ( + { + set( + activeTabIdComponentState.atomFamily({ + instanceId: `${PAGE_LAYOUT_TEST_INSTANCE_ID}-tab-list`, + }), + 'tab-1', + ); + }} + > + {children} + + ), + }, + ); + + const newLayouts = { + desktop: [ + { i: 'widget-1', x: 2, y: 3, w: 4, h: 5 }, + { i: 'widget-2', x: 6, y: 7, w: 8, h: 9 }, + ], + mobile: [ + { i: 'widget-1', x: 0, y: 0, w: 1, h: 5 }, + { i: 'widget-2', x: 0, y: 5, w: 1, h: 9 }, + ], + }; + + act(() => { + result.current.handler.handleLayoutChange([], newLayouts); + }); + + expect(result.current.layouts['tab-1']).toEqual(newLayouts); + expect(result.current.layouts['tab-2']).toBeUndefined(); + }); + + it('should isolate layouts between different tabs', () => { + const { result } = renderHook( + () => ({ + handler: usePageLayoutHandleLayoutChange(PAGE_LAYOUT_TEST_INSTANCE_ID), + layouts: useRecoilComponentValue( + pageLayoutCurrentLayoutsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }) => ( + { + set( + activeTabIdComponentState.atomFamily({ + instanceId: `${PAGE_LAYOUT_TEST_INSTANCE_ID}-tab-list`, + }), + 'tab-1', + ); + }} + > + {children} + + ), + }, + ); + + const tab1Layouts = { + desktop: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2 }], + mobile: [{ i: 'widget-1', x: 0, y: 0, w: 1, h: 2 }], + }; + + act(() => { + result.current.handler.handleLayoutChange([], tab1Layouts); + }); + + // Unfortunately we can't properly test tab switching with the current renderHook API + // since we can't change the activeTabId after initialization in a simple way + expect(result.current.layouts['tab-1']).toEqual(tab1Layouts); + }); + + it('should not update layouts when activeTabId is null', () => { + const { result } = renderHook( + () => ({ + handler: usePageLayoutHandleLayoutChange(PAGE_LAYOUT_TEST_INSTANCE_ID), + layouts: useRecoilComponentValue( + pageLayoutCurrentLayoutsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: PageLayoutTestWrapper, + }, + ); + + const newLayouts = { + desktop: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2 }], + mobile: [{ i: 'widget-1', x: 0, y: 0, w: 1, h: 2 }], + }; + + act(() => { + result.current.handler.handleLayoutChange([], newLayouts); + }); + + expect(Object.keys(result.current.layouts)).toHaveLength(0); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useStartPageLayoutDragSelection.test.ts b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useStartPageLayoutDragSelection.test.ts deleted file mode 100644 index 296345a460..0000000000 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useStartPageLayoutDragSelection.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { act, renderHook } from '@testing-library/react'; -import { type ReactNode } from 'react'; -import { RecoilRoot, useRecoilValue } from 'recoil'; -import { pageLayoutSelectedCellsState } from '../../states/pageLayoutSelectedCellsState'; -import { useStartPageLayoutDragSelection } from '../useStartPageLayoutDragSelection'; - -describe('useStartPageLayoutDragSelection', () => { - it('should clear selected cells when starting drag selection', () => { - const { result } = renderHook( - () => ({ - startDragSelection: useStartPageLayoutDragSelection(), - selectedCells: useRecoilValue(pageLayoutSelectedCellsState), - }), - { - wrapper: ({ children }: { children: ReactNode }) => - RecoilRoot({ - initializeState: ({ set }) => { - set(pageLayoutSelectedCellsState, new Set(['cell-1', 'cell-2'])); - }, - children, - }), - }, - ); - - expect(result.current.selectedCells.size).toBe(2); - expect(result.current.selectedCells.has('cell-1')).toBe(true); - expect(result.current.selectedCells.has('cell-2')).toBe(true); - - act(() => { - result.current.startDragSelection.startPageLayoutDragSelection(); - }); - - expect(result.current.selectedCells.size).toBe(0); - }); - - it('should return a function', () => { - const { result } = renderHook(() => useStartPageLayoutDragSelection(), { - wrapper: RecoilRoot, - }); - - expect(typeof result.current.startPageLayoutDragSelection).toBe('function'); - }); - - it('should handle multiple calls correctly', () => { - const { result } = renderHook( - () => ({ - startDragSelection: useStartPageLayoutDragSelection(), - selectedCells: useRecoilValue(pageLayoutSelectedCellsState), - }), - { - wrapper: ({ children }: { children: ReactNode }) => - RecoilRoot({ - initializeState: ({ set }) => { - set(pageLayoutSelectedCellsState, new Set(['cell-1'])); - }, - children, - }), - }, - ); - - expect(result.current.selectedCells.size).toBe(1); - - act(() => { - result.current.startDragSelection.startPageLayoutDragSelection(); - }); - expect(result.current.selectedCells.size).toBe(0); - - act(() => { - result.current.startDragSelection.startPageLayoutDragSelection(); - }); - expect(result.current.selectedCells.size).toBe(0); - }); -}); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useStartPageLayoutDragSelection.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useStartPageLayoutDragSelection.test.tsx new file mode 100644 index 0000000000..3ee03440af --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useStartPageLayoutDragSelection.test.tsx @@ -0,0 +1,108 @@ +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { act, renderHook } from '@testing-library/react'; +import { type ReactNode } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { pageLayoutSelectedCellsComponentState } from '../../states/pageLayoutSelectedCellsComponentState'; +import { useStartPageLayoutDragSelection } from '../useStartPageLayoutDragSelection'; +import { + PAGE_LAYOUT_TEST_INSTANCE_ID, + PageLayoutTestWrapper, +} from './PageLayoutTestWrapper'; + +const createInitializeState = + (initialSelectedCells?: Set) => + ({ set }: { set: any }) => { + if (isDefined(initialSelectedCells)) { + set( + pageLayoutSelectedCellsComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + initialSelectedCells, + ); + } + }; + +describe('useStartPageLayoutDragSelection', () => { + it('should clear selected cells when starting drag selection', () => { + const { result } = renderHook( + () => ({ + startDragSelection: useStartPageLayoutDragSelection( + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + selectedCells: useRecoilComponentValue( + pageLayoutSelectedCellsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }, + ); + + expect(result.current.selectedCells.size).toBe(2); + expect(result.current.selectedCells.has('cell-1')).toBe(true); + expect(result.current.selectedCells.has('cell-2')).toBe(true); + + act(() => { + result.current.startDragSelection.startPageLayoutDragSelection(); + }); + + expect(result.current.selectedCells.size).toBe(0); + }); + + it('should return a function', () => { + const { result } = renderHook( + () => useStartPageLayoutDragSelection(PAGE_LAYOUT_TEST_INSTANCE_ID), + { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }, + ); + + expect(typeof result.current.startPageLayoutDragSelection).toBe('function'); + }); + + it('should handle multiple calls correctly', () => { + const { result } = renderHook( + () => ({ + startDragSelection: useStartPageLayoutDragSelection( + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + selectedCells: useRecoilComponentValue( + pageLayoutSelectedCellsComponentState, + PAGE_LAYOUT_TEST_INSTANCE_ID, + ), + }), + { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }, + ); + + expect(result.current.selectedCells.size).toBe(1); + + act(() => { + result.current.startDragSelection.startPageLayoutDragSelection(); + }); + expect(result.current.selectedCells.size).toBe(0); + + act(() => { + result.current.startDragSelection.startPageLayoutDragSelection(); + }); + expect(result.current.selectedCells.size).toBe(0); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useChangePageLayoutDragSelection.ts b/packages/twenty-front/src/modules/page-layout/hooks/useChangePageLayoutDragSelection.ts index 07d58859d3..36f782bd95 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useChangePageLayoutDragSelection.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useChangePageLayoutDragSelection.ts @@ -1,7 +1,22 @@ +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; -import { pageLayoutSelectedCellsState } from '../states/pageLayoutSelectedCellsState'; +import { pageLayoutSelectedCellsComponentState } from '../states/pageLayoutSelectedCellsComponentState'; + +export const useChangePageLayoutDragSelection = ( + pageLayoutIdFromProps?: string, +) => { + const pageLayoutId = useAvailableComponentInstanceIdOrThrow( + PageLayoutComponentInstanceContext, + pageLayoutIdFromProps, + ); + + const pageLayoutSelectedCellsState = useRecoilComponentCallbackState( + pageLayoutSelectedCellsComponentState, + pageLayoutId, + ); -export const useChangePageLayoutDragSelection = () => { const changePageLayoutDragSelection = useRecoilCallback( ({ set }) => (cellId: string, selected: boolean) => { @@ -15,7 +30,7 @@ export const useChangePageLayoutDragSelection = () => { return newSet; }); }, - [], + [pageLayoutSelectedCellsState], ); return { changePageLayoutDragSelection }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts index 97ca591902..e8d99deacb 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts @@ -1,21 +1,46 @@ -import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '@/page-layout/constants/SettingsPageLayoutTabsInstanceId'; -import { pageLayoutCurrentLayoutsState } from '@/page-layout/states/pageLayoutCurrentLayoutsState'; -import { pageLayoutDraftState } from '@/page-layout/states/pageLayoutDraftState'; -import { pageLayoutDraggedAreaState } from '@/page-layout/states/pageLayoutDraggedAreaState'; +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState'; import { type PageLayoutWidgetWithData } from '@/page-layout/types/pageLayoutTypes'; import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; -import { createUpdatedTabLayouts } from '@/page-layout/utils/createUpdatedTabLayouts'; 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'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useRecoilCallback } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; import { WidgetType } from '~/generated/graphql'; -export const useCreatePageLayoutIframeWidget = () => { +export const useCreatePageLayoutIframeWidget = ( + pageLayoutIdFromProps?: string, +) => { + const pageLayoutId = useAvailableComponentInstanceIdOrThrow( + PageLayoutComponentInstanceContext, + pageLayoutIdFromProps, + ); + const activeTabId = useRecoilComponentValue( activeTabIdComponentState, - SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID, + getTabListInstanceIdFromPageLayoutId(pageLayoutId), + ); + + const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState( + pageLayoutCurrentLayoutsComponentState, + pageLayoutId, + ); + + const pageLayoutDraggedAreaState = useRecoilComponentCallbackState( + pageLayoutDraggedAreaComponentState, + pageLayoutId, + ); + + const pageLayoutDraftState = useRecoilComponentCallbackState( + pageLayoutDraftComponentState, + pageLayoutId, ); const createPageLayoutIframeWidget = useRecoilCallback( @@ -24,6 +49,7 @@ export const useCreatePageLayoutIframeWidget = () => { const allTabLayouts = snapshot .getLoadable(pageLayoutCurrentLayoutsState) .getValue(); + const pageLayoutDraggedArea = snapshot .getLoadable(pageLayoutDraggedAreaState) .getValue(); @@ -68,11 +94,12 @@ export const useCreatePageLayoutIframeWidget = () => { h: position.h, }; - const updatedLayouts = createUpdatedTabLayouts( + const updatedLayouts = getUpdatedTabLayouts( allTabLayouts, activeTabId, newLayout, ); + set(pageLayoutCurrentLayoutsState, updatedLayouts); set(pageLayoutDraftState, (prev) => ({ @@ -82,7 +109,12 @@ export const useCreatePageLayoutIframeWidget = () => { set(pageLayoutDraggedAreaState, null); }, - [activeTabId], + [ + activeTabId, + pageLayoutCurrentLayoutsState, + pageLayoutDraftState, + pageLayoutDraggedAreaState, + ], ); return { createPageLayoutIframeWidget }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutTab.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutTab.ts index 1293eebfc7..fa625226ff 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutTab.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutTab.ts @@ -1,11 +1,29 @@ +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; -import { pageLayoutCurrentLayoutsState } from '../states/pageLayoutCurrentLayoutsState'; -import { pageLayoutDraftState } from '../states/pageLayoutDraftState'; +import { pageLayoutCurrentLayoutsComponentState } from '../states/pageLayoutCurrentLayoutsComponentState'; +import { pageLayoutDraftComponentState } from '../states/pageLayoutDraftComponentState'; import { type PageLayoutTabWithData } from '../types/pageLayoutTypes'; -import { createEmptyTabLayout } from '../utils/createEmptyTabLayout'; +import { getEmptyTabLayout } from '../utils/getEmptyTabLayout'; + +export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => { + const pageLayoutId = useAvailableComponentInstanceIdOrThrow( + PageLayoutComponentInstanceContext, + pageLayoutIdFromProps, + ); + + const pageLayoutDraftState = useRecoilComponentCallbackState( + pageLayoutDraftComponentState, + pageLayoutId, + ); + + const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState( + pageLayoutCurrentLayoutsComponentState, + pageLayoutId, + ); -export const useCreatePageLayoutTab = () => { const createPageLayoutTab = useRecoilCallback( ({ snapshot, set }) => (title?: string): string => { @@ -34,12 +52,12 @@ export const useCreatePageLayoutTab = () => { })); set(pageLayoutCurrentLayoutsState, (prev) => - createEmptyTabLayout(prev, newTabId), + getEmptyTabLayout(prev, newTabId), ); return newTabId; }, - [], + [pageLayoutCurrentLayoutsState, pageLayoutDraftState], ); return { createPageLayoutTab }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutWidget.ts index f3e9429e25..c7b1940449 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutWidget.ts @@ -1,48 +1,76 @@ -import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '@/page-layout/constants/SettingsPageLayoutTabsInstanceId'; import { type GraphType } from '@/page-layout/mocks/mockWidgets'; -import { pageLayoutCurrentLayoutsState } from '@/page-layout/states/pageLayoutCurrentLayoutsState'; -import { pageLayoutDraftState } from '@/page-layout/states/pageLayoutDraftState'; -import { pageLayoutDraggedAreaState } from '@/page-layout/states/pageLayoutDraggedAreaState'; +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState'; import { type PageLayoutWidgetWithData } from '@/page-layout/types/pageLayoutTypes'; import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; -import { createUpdatedTabLayouts } from '@/page-layout/utils/createUpdatedTabLayouts'; import { getDefaultWidgetData, getWidgetSize, getWidgetTitle, } from '@/page-layout/utils/getDefaultWidgetData'; 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 { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; import { type WidgetType } from '~/generated/graphql'; -export const useCreatePageLayoutWidget = () => { - const activeTabId = useRecoilComponentValue( +export const useCreatePageLayoutWidget = (pageLayoutIdFromProps?: string) => { + const pageLayoutId = useAvailableComponentInstanceIdOrThrow( + PageLayoutComponentInstanceContext, + pageLayoutIdFromProps, + ); + + const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId); + + const activeTabIdState = useRecoilComponentCallbackState( activeTabIdComponentState, - SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID, + tabListInstanceId, + ); + + const pageLayoutDraftState = useRecoilComponentCallbackState( + pageLayoutDraftComponentState, + pageLayoutId, + ); + + const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState( + pageLayoutCurrentLayoutsComponentState, + pageLayoutId, + ); + + const pageLayoutDraggedAreaState = useRecoilComponentCallbackState( + pageLayoutDraggedAreaComponentState, + pageLayoutId, ); const createPageLayoutWidget = useRecoilCallback( ({ snapshot, set }) => (widgetType: WidgetType, graphType: GraphType) => { + const activeTabId = snapshot.getLoadable(activeTabIdState).getValue(); + + if (!activeTabId) { + return; + } + const widgetData = getDefaultWidgetData(graphType); const pageLayoutDraft = snapshot .getLoadable(pageLayoutDraftState) .getValue(); + const allTabLayouts = snapshot .getLoadable(pageLayoutCurrentLayoutsState) .getValue(); + const pageLayoutDraggedArea = snapshot .getLoadable(pageLayoutDraggedAreaState) .getValue(); - if (!activeTabId) { - return; - } - const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets); const existingWidgetCount = allWidgets.filter( (w) => @@ -86,7 +114,7 @@ export const useCreatePageLayoutWidget = () => { h: position.h, }; - const updatedLayouts = createUpdatedTabLayouts( + const updatedLayouts = getUpdatedTabLayouts( allTabLayouts, activeTabId, newLayout, @@ -100,7 +128,12 @@ export const useCreatePageLayoutWidget = () => { set(pageLayoutDraggedAreaState, null); }, - [activeTabId], + [ + activeTabIdState, + pageLayoutCurrentLayoutsState, + pageLayoutDraftState, + pageLayoutDraggedAreaState, + ], ); return { createPageLayoutWidget }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCurrentPageLayout.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCurrentPageLayout.ts new file mode 100644 index 0000000000..81d4bae833 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCurrentPageLayout.ts @@ -0,0 +1,24 @@ +import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; + +export const useCurrentPageLayout = () => { + const pageLayoutPersisted = useRecoilComponentValue( + pageLayoutPersistedComponentState, + ); + + const pageLayoutDraft = useRecoilComponentValue( + pageLayoutDraftComponentState, + ); + + const isPageLayoutInEditMode = useRecoilComponentValue( + isPageLayoutInEditModeComponentState, + ); + + const currentPageLayout = isPageLayoutInEditMode + ? pageLayoutDraft + : pageLayoutPersisted; + + return { currentPageLayout }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutWidget.ts index 1365f88a7b..97ca43d671 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutWidget.ts @@ -1,11 +1,29 @@ +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -import { pageLayoutCurrentLayoutsState } from '../states/pageLayoutCurrentLayoutsState'; -import { pageLayoutDraftState } from '../states/pageLayoutDraftState'; +import { pageLayoutCurrentLayoutsComponentState } from '../states/pageLayoutCurrentLayoutsComponentState'; +import { pageLayoutDraftComponentState } from '../states/pageLayoutDraftComponentState'; import { removeWidgetFromTab } from '../utils/removeWidgetFromTab'; import { removeWidgetLayoutFromTab } from '../utils/removeWidgetLayoutFromTab'; -export const useDeletePageLayoutWidget = () => { +export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => { + const pageLayoutId = useAvailableComponentInstanceIdOrThrow( + PageLayoutComponentInstanceContext, + pageLayoutIdFromProps, + ); + + const pageLayoutDraftState = useRecoilComponentCallbackState( + pageLayoutDraftComponentState, + pageLayoutId, + ); + + const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState( + pageLayoutCurrentLayoutsComponentState, + pageLayoutId, + ); + const deletePageLayoutWidget = useRecoilCallback( ({ snapshot, set }) => (widgetId: string) => { @@ -35,7 +53,7 @@ export const useDeletePageLayoutWidget = () => { })); } }, - [], + [pageLayoutCurrentLayoutsState, pageLayoutDraftState], ); return { deletePageLayoutWidget }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts new file mode 100644 index 0000000000..d50ac1339b --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts @@ -0,0 +1,49 @@ +import { useCallback } from 'react'; + +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; + +import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu'; +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { t } from '@lingui/core/macro'; +import { WidgetType } from '~/generated/graphql'; + +export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => { + const pageLayoutId = useAvailableComponentInstanceIdOrThrow( + PageLayoutComponentInstanceContext, + pageLayoutIdFromProps, + ); + + const setPageLayoutEditingWidgetId = useSetRecoilComponentState( + pageLayoutEditingWidgetIdComponentState, + pageLayoutId, + ); + + const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu(); + + const handleEditWidget = useCallback( + ({ + widgetId, + widgetType, + }: { + widgetId: string; + widgetType: WidgetType; + }) => { + setPageLayoutEditingWidgetId(widgetId); + + if (widgetType === WidgetType.IFRAME) { + navigatePageLayoutCommandMenu({ + commandMenuPage: CommandMenuPages.PageLayoutIframeConfig, + pageTitle: t`Edit iFrame`, + }); + } + }, + [setPageLayoutEditingWidgetId, navigatePageLayoutCommandMenu], + ); + + return { + handleEditWidget, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useEndPageLayoutDragSelection.ts b/packages/twenty-front/src/modules/page-layout/hooks/useEndPageLayoutDragSelection.ts index 5c579f24ec..88200a259c 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useEndPageLayoutDragSelection.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useEndPageLayoutDragSelection.ts @@ -1,14 +1,32 @@ -import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; +import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -import { IconAppWindow } from 'twenty-ui/display'; -import { pageLayoutDraggedAreaState } from '../states/pageLayoutDraggedAreaState'; -import { pageLayoutSelectedCellsState } from '../states/pageLayoutSelectedCellsState'; +import { pageLayoutSelectedCellsComponentState } from '../states/pageLayoutSelectedCellsComponentState'; import { calculateGridBoundsFromSelectedCells } from '../utils/calculateGridBoundsFromSelectedCells'; -export const useEndPageLayoutDragSelection = () => { - const { navigateCommandMenu } = useNavigateCommandMenu(); +export const useEndPageLayoutDragSelection = ( + pageLayoutIdFromProps?: string, +) => { + const pageLayoutId = useAvailableComponentInstanceIdOrThrow( + PageLayoutComponentInstanceContext, + pageLayoutIdFromProps, + ); + + const pageLayoutSelectedCellsState = useRecoilComponentCallbackState( + pageLayoutSelectedCellsComponentState, + pageLayoutId, + ); + const pageLayoutDraggedAreaState = useRecoilComponentCallbackState( + pageLayoutDraggedAreaComponentState, + pageLayoutId, + ); + + const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu(); const endPageLayoutDragSelection = useRecoilCallback( ({ snapshot, set }) => @@ -25,18 +43,19 @@ export const useEndPageLayoutDragSelection = () => { if (isDefined(draggedBounds)) { set(pageLayoutDraggedAreaState, draggedBounds); - navigateCommandMenu({ - page: CommandMenuPages.PageLayoutWidgetTypeSelect, - pageTitle: 'Add Widget', - pageIcon: IconAppWindow, - resetNavigationStack: true, + navigatePageLayoutCommandMenu({ + commandMenuPage: CommandMenuPages.PageLayoutWidgetTypeSelect, }); - - set(pageLayoutSelectedCellsState, new Set()); } } + + set(pageLayoutSelectedCellsState, new Set()); }, - [navigateCommandMenu], + [ + navigatePageLayoutCommandMenu, + pageLayoutDraggedAreaState, + pageLayoutSelectedCellsState, + ], ); return { endPageLayoutDragSelection }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutDraftState.ts b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutDraftState.ts index 41c82a8a2c..762916a15d 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutDraftState.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutDraftState.ts @@ -1,15 +1,29 @@ -import { useRecoilState, useRecoilValue } from 'recoil'; +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; -import { pageLayoutDraftState } from '../states/pageLayoutDraftState'; -import { pageLayoutPersistedState } from '../states/pageLayoutPersistedState'; +import { pageLayoutDraftComponentState } from '../states/pageLayoutDraftComponentState'; +import { pageLayoutPersistedComponentState } from '../states/pageLayoutPersistedComponentState'; -export const usePageLayoutDraftState = () => { - const [pageLayoutDraft, setPageLayoutDraft] = - useRecoilState(pageLayoutDraftState); - const pageLayoutPersisted = useRecoilValue(pageLayoutPersistedState); +export const usePageLayoutDraftState = (pageLayoutIdFromProps?: string) => { + const pageLayoutId = useAvailableComponentInstanceIdOrThrow( + PageLayoutComponentInstanceContext, + pageLayoutIdFromProps, + ); + + const [pageLayoutDraft, setPageLayoutDraft] = useRecoilComponentState( + pageLayoutDraftComponentState, + pageLayoutId, + ); + const pageLayoutPersisted = useRecoilComponentValue( + pageLayoutPersistedComponentState, + pageLayoutId, + ); const isDirty = pageLayoutPersisted ? !isDeeplyEqual(pageLayoutDraft, { + id: pageLayoutPersisted.id, name: pageLayoutPersisted.name, type: pageLayoutPersisted.type, objectMetadataId: pageLayoutPersisted.objectMetadataId, diff --git a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutHandleLayoutChange.ts b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutHandleLayoutChange.ts index 6cbb965603..f137225e49 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutHandleLayoutChange.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutHandleLayoutChange.ts @@ -1,16 +1,47 @@ +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId'; +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { type Layout, type Layouts } from 'react-grid-layout'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -import { pageLayoutCurrentLayoutsState } from '../states/pageLayoutCurrentLayoutsState'; -import { pageLayoutDraftState } from '../states/pageLayoutDraftState'; -import { type PageLayoutWidgetWithData } from '../types/pageLayoutTypes'; +import { pageLayoutCurrentLayoutsComponentState } from '../states/pageLayoutCurrentLayoutsComponentState'; +import { pageLayoutDraftComponentState } from '../states/pageLayoutDraftComponentState'; import { convertLayoutsToWidgets } from '../utils/convertLayoutsToWidgets'; -export const usePageLayoutHandleLayoutChange = (activeTabId: string | null) => { +export const usePageLayoutHandleLayoutChange = ( + pageLayoutIdFromProps?: string, +) => { + const pageLayoutId = useAvailableComponentInstanceIdOrThrow( + PageLayoutComponentInstanceContext, + pageLayoutIdFromProps, + ); + + const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId); + + const activeTabIdState = useRecoilComponentCallbackState( + activeTabIdComponentState, + tabListInstanceId, + ); + + const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState( + pageLayoutCurrentLayoutsComponentState, + pageLayoutId, + ); + + const pageLayoutDraftState = useRecoilComponentCallbackState( + pageLayoutDraftComponentState, + pageLayoutId, + ); + const handleLayoutChange = useRecoilCallback( ({ snapshot, set }) => (_: Layout[], allLayouts: Layouts) => { + const activeTabId = snapshot.getLoadable(activeTabIdState).getValue(); + if (!isDefined(activeTabId)) return; + const currentTabLayouts = snapshot .getLoadable(pageLayoutCurrentLayoutsState) .getValue(); @@ -27,7 +58,9 @@ export const usePageLayoutHandleLayoutChange = (activeTabId: string | null) => { const currentTab = pageLayoutDraft.tabs.find( (tab) => tab.id === activeTabId, ); + if (!currentTab) return; + const updatedWidgets = convertLayoutsToWidgets( currentTab.widgets, allLayouts, @@ -38,7 +71,7 @@ export const usePageLayoutHandleLayoutChange = (activeTabId: string | null) => { ...prev, tabs: prev.tabs.map((tab) => { if (tab.id === activeTabId) { - const tabWidgets: PageLayoutWidgetWithData[] = updatedWidgets + const tabWidgets = updatedWidgets .filter((w) => w.pageLayoutTabId === activeTabId) .map((widget) => ({ id: widget.id, @@ -65,7 +98,7 @@ export const usePageLayoutHandleLayoutChange = (activeTabId: string | null) => { })); } }, - [activeTabId], + [activeTabIdState, pageLayoutCurrentLayoutsState, pageLayoutDraftState], ); return { handleLayoutChange }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutSaveHandler.ts b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutSaveHandler.ts deleted file mode 100644 index b0fd37dead..0000000000 --- a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutSaveHandler.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { savedPageLayoutsState } from '@/page-layout/states/savedPageLayoutsState'; -import { - type PageLayoutWidgetWithData, - type PageLayoutWithData, -} from '@/page-layout/types/pageLayoutTypes'; -import { useParams } from 'react-router-dom'; -import { useRecoilCallback } from 'recoil'; -import { SettingsPath } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { v4 as uuidv4 } from 'uuid'; -import { useNavigateSettings } from '~/hooks/useNavigateSettings'; -import { pageLayoutDraftState } from '../states/pageLayoutDraftState'; -import { pageLayoutPersistedState } from '../states/pageLayoutPersistedState'; - -export const usePageLayoutSaveHandler = () => { - const navigateSettings = useNavigateSettings(); - const { id } = useParams<{ id: string }>(); - const isEditMode = id && id !== 'new'; - - const savePageLayout = useRecoilCallback( - ({ snapshot, set }) => - async (widgetsWithPositions?: PageLayoutWidgetWithData[]) => { - const pageLayoutDraft = snapshot - .getLoadable(pageLayoutDraftState) - .getValue(); - const savedPageLayouts = snapshot - .getLoadable(savedPageLayoutsState) - .getValue(); - - const existingLayout = isEditMode - ? savedPageLayouts.find((layout) => layout.id === id) - : undefined; - - const updatedTabs = widgetsWithPositions - ? pageLayoutDraft.tabs.map((tab) => ({ - ...tab, - widgets: widgetsWithPositions.filter( - (w) => w.pageLayoutTabId === tab.id, - ), - })) - : pageLayoutDraft.tabs; - - const layoutToSave: PageLayoutWithData = { - id: isEditMode ? id : uuidv4(), - name: pageLayoutDraft.name, - type: pageLayoutDraft.type, - objectMetadataId: pageLayoutDraft.objectMetadataId, - tabs: updatedTabs, - createdAt: isEditMode - ? (existingLayout?.createdAt ?? new Date().toISOString()) - : new Date().toISOString(), - updatedAt: new Date().toISOString(), - deletedAt: null, - }; - - set(savedPageLayoutsState, (prev) => { - if (isDefined(isEditMode)) { - return prev.map((layout) => - layout.id === id ? layoutToSave : layout, - ); - } - return [...prev, layoutToSave]; - }); - - set(pageLayoutPersistedState, layoutToSave); - - navigateSettings(SettingsPath.PageLayout); - }, - [isEditMode, id, navigateSettings], - ); - - return { savePageLayout }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useStartPageLayoutDragSelection.ts b/packages/twenty-front/src/modules/page-layout/hooks/useStartPageLayoutDragSelection.ts index f109425dcd..6cbe8df4bd 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useStartPageLayoutDragSelection.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useStartPageLayoutDragSelection.ts @@ -1,13 +1,28 @@ +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; -import { pageLayoutSelectedCellsState } from '../states/pageLayoutSelectedCellsState'; +import { pageLayoutSelectedCellsComponentState } from '../states/pageLayoutSelectedCellsComponentState'; + +export const useStartPageLayoutDragSelection = ( + pageLayoutIdFromProps?: string, +) => { + const pageLayoutId = useAvailableComponentInstanceIdOrThrow( + PageLayoutComponentInstanceContext, + pageLayoutIdFromProps, + ); + + const pageLayoutSelectedCellsState = useRecoilComponentCallbackState( + pageLayoutSelectedCellsComponentState, + pageLayoutId, + ); -export const useStartPageLayoutDragSelection = () => { const startPageLayoutDragSelection = useRecoilCallback( ({ set }) => () => { set(pageLayoutSelectedCellsState, new Set()); }, - [], + [pageLayoutSelectedCellsState], ); return { startPageLayoutDragSelection }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWidget.ts index 9e027c8ed0..7d20578b04 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWidget.ts @@ -1,8 +1,21 @@ -import { pageLayoutDraftState } from '@/page-layout/states/pageLayoutDraftState'; +import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { type PageLayoutWidgetWithData } from '@/page-layout/types/pageLayoutTypes'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; -export const useUpdatePageLayoutWidget = () => { +export const useUpdatePageLayoutWidget = (pageLayoutIdFromProps?: string) => { + const pageLayoutId = useAvailableComponentInstanceIdOrThrow( + PageLayoutComponentInstanceContext, + pageLayoutIdFromProps, + ); + + const pageLayoutDraftState = useRecoilComponentCallbackState( + pageLayoutDraftComponentState, + pageLayoutId, + ); + const updatePageLayoutWidget = useRecoilCallback( ({ set }) => (widgetId: string, updates: Partial) => { @@ -16,7 +29,7 @@ export const useUpdatePageLayoutWidget = () => { })), })); }, - [], + [pageLayoutDraftState], ); return { updatePageLayoutWidget }; diff --git a/packages/twenty-front/src/modules/page-layout/states/contexts/PageLayoutComponentInstanceContext.ts b/packages/twenty-front/src/modules/page-layout/states/contexts/PageLayoutComponentInstanceContext.ts new file mode 100644 index 0000000000..2f367f6086 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/contexts/PageLayoutComponentInstanceContext.ts @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const PageLayoutComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/page-layout/states/isPageLayoutInEditModeComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/isPageLayoutInEditModeComponentState.ts new file mode 100644 index 0000000000..2e411dfccd --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/isPageLayoutInEditModeComponentState.ts @@ -0,0 +1,10 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const isPageLayoutInEditModeComponentState = + createComponentState({ + key: 'isPageLayoutInEditModeComponentState', + defaultValue: false, + componentInstanceContext: PageLayoutComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutCurrentBreakpointComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutCurrentBreakpointComponentState.ts new file mode 100644 index 0000000000..9b45961173 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/pageLayoutCurrentBreakpointComponentState.ts @@ -0,0 +1,11 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { type PageLayoutBreakpoint } from '../constants/PageLayoutBreakpoints'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const pageLayoutCurrentBreakpointComponentState = + createComponentState({ + key: 'pageLayoutCurrentBreakpointComponentState', + defaultValue: 'desktop', + componentInstanceContext: PageLayoutComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutCurrentBreakpointState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutCurrentBreakpointState.ts deleted file mode 100644 index 665d60995a..0000000000 --- a/packages/twenty-front/src/modules/page-layout/states/pageLayoutCurrentBreakpointState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; -import { type PageLayoutBreakpoint } from '../constants/PageLayoutBreakpoints'; - -export const pageLayoutCurrentBreakpointState = - createState({ - key: 'pageLayoutCurrentBreakpointState', - defaultValue: 'desktop', - }); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutCurrentLayoutsComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutCurrentLayoutsComponentState.ts new file mode 100644 index 0000000000..894ccb8415 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/pageLayoutCurrentLayoutsComponentState.ts @@ -0,0 +1,11 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +import { type TabLayouts } from '@/page-layout/types/tab-layouts'; +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const pageLayoutCurrentLayoutsComponentState = + createComponentState({ + key: 'pageLayoutCurrentLayoutsComponentState', + defaultValue: {}, + componentInstanceContext: PageLayoutComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutCurrentLayoutsState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutCurrentLayoutsState.ts deleted file mode 100644 index 4f9f945fc2..0000000000 --- a/packages/twenty-front/src/modules/page-layout/states/pageLayoutCurrentLayoutsState.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type Layouts } from 'react-grid-layout'; -import { createState } from 'twenty-ui/utilities'; - -export type TabLayouts = Record; - -export const pageLayoutCurrentLayoutsState = createState({ - key: 'pageLayoutCurrentLayoutsState', - defaultValue: {}, -}); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraftComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraftComponentState.ts new file mode 100644 index 0000000000..0d824eda53 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraftComponentState.ts @@ -0,0 +1,18 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { PageLayoutType } from '~/generated/graphql'; + +import { type DraftPageLayout } from '@/page-layout/types/draft-page-layout'; +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const pageLayoutDraftComponentState = + createComponentState({ + key: 'pageLayoutDraftComponentState', + defaultValue: { + id: '', + name: '', + type: PageLayoutType.DASHBOARD, + objectMetadataId: null, + tabs: [], + }, + componentInstanceContext: PageLayoutComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraftState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraftState.ts deleted file mode 100644 index e268f4d6d1..0000000000 --- a/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraftState.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; -import { PageLayoutType } from '~/generated/graphql'; -import { type PageLayoutWithData } from '../types/pageLayoutTypes'; - -export type DraftPageLayout = Omit< - PageLayoutWithData, - 'id' | 'createdAt' | 'updatedAt' | 'deletedAt' ->; - -export const pageLayoutDraftState = createState({ - key: 'pageLayoutDraftState', - defaultValue: { - name: '', - type: PageLayoutType.DASHBOARD, - objectMetadataId: null, - tabs: [], - }, -}); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraggedAreaComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraggedAreaComponentState.ts new file mode 100644 index 0000000000..0555927f4d --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraggedAreaComponentState.ts @@ -0,0 +1,11 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +import { type PageLayoutDraggedArea } from '@/page-layout/types/page-layout-dragged-area'; +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const pageLayoutDraggedAreaComponentState = + createComponentState({ + key: 'pageLayoutDraggedAreaComponentState', + defaultValue: null, + componentInstanceContext: PageLayoutComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraggedAreaState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraggedAreaState.ts deleted file mode 100644 index d0c1a37b66..0000000000 --- a/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraggedAreaState.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; - -type DraggedArea = { - x: number; - y: number; - w: number; - h: number; -} | null; - -export const pageLayoutDraggedAreaState = createState({ - key: 'pageLayoutDraggedAreaState', - defaultValue: null, -}); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutEditingWidgetIdComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutEditingWidgetIdComponentState.ts new file mode 100644 index 0000000000..c276e9b92b --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/pageLayoutEditingWidgetIdComponentState.ts @@ -0,0 +1,11 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const pageLayoutEditingWidgetIdComponentState = createComponentState< + string | null +>({ + key: 'pageLayoutEditingWidgetIdComponentState', + defaultValue: null, + componentInstanceContext: PageLayoutComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutEditingWidgetIdState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutEditingWidgetIdState.ts deleted file mode 100644 index 8c35527562..0000000000 --- a/packages/twenty-front/src/modules/page-layout/states/pageLayoutEditingWidgetIdState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; - -export const pageLayoutEditingWidgetIdState = createState({ - key: 'pageLayoutEditingWidgetIdState', - defaultValue: null, -}); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutPersistedComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutPersistedComponentState.ts new file mode 100644 index 0000000000..0f4ba738af --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/pageLayoutPersistedComponentState.ts @@ -0,0 +1,12 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { type PageLayoutWithData } from '../types/pageLayoutTypes'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const pageLayoutPersistedComponentState = createComponentState< + PageLayoutWithData | undefined +>({ + key: 'pageLayoutPersistedComponentState', + defaultValue: undefined, + componentInstanceContext: PageLayoutComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutPersistedState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutPersistedState.ts deleted file mode 100644 index def8b9b951..0000000000 --- a/packages/twenty-front/src/modules/page-layout/states/pageLayoutPersistedState.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; -import { type PageLayoutWithData } from '../types/pageLayoutTypes'; - -export const pageLayoutPersistedState = createState< - PageLayoutWithData | undefined ->({ - key: 'pageLayoutPersistedState', - defaultValue: undefined, -}); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutSelectedCellsComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutSelectedCellsComponentState.ts new file mode 100644 index 0000000000..b7ddad373c --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/pageLayoutSelectedCellsComponentState.ts @@ -0,0 +1,11 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const pageLayoutSelectedCellsComponentState = createComponentState< + Set +>({ + key: 'pageLayoutSelectedCellsComponentState', + defaultValue: new Set(), + componentInstanceContext: PageLayoutComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutSelectedCellsState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutSelectedCellsState.ts deleted file mode 100644 index 26f4200c0b..0000000000 --- a/packages/twenty-front/src/modules/page-layout/states/pageLayoutSelectedCellsState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; - -export const pageLayoutSelectedCellsState = createState>({ - key: 'pageLayoutSelectedCellsState', - defaultValue: new Set(), -}); diff --git a/packages/twenty-front/src/modules/page-layout/states/savedPageLayoutsComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/savedPageLayoutsComponentState.ts new file mode 100644 index 0000000000..5269b53175 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/savedPageLayoutsComponentState.ts @@ -0,0 +1,12 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { type PageLayoutWithData } from '../types/pageLayoutTypes'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const savedPageLayoutsComponentState = createComponentState< + PageLayoutWithData[] +>({ + key: 'savedPageLayoutsComponentState', + defaultValue: [], + componentInstanceContext: PageLayoutComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/page-layout/states/savedPageLayoutsState.ts b/packages/twenty-front/src/modules/page-layout/states/savedPageLayoutsState.ts deleted file mode 100644 index fb82c445ae..0000000000 --- a/packages/twenty-front/src/modules/page-layout/states/savedPageLayoutsState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; -import { type PageLayoutWithData } from '../types/pageLayoutTypes'; - -export const savedPageLayoutsState = createState({ - key: 'savedPageLayoutsState', - defaultValue: [], -}); diff --git a/packages/twenty-front/src/modules/page-layout/types/draft-page-layout.ts b/packages/twenty-front/src/modules/page-layout/types/draft-page-layout.ts new file mode 100644 index 0000000000..e2bef1cd62 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/types/draft-page-layout.ts @@ -0,0 +1,6 @@ +import { type PageLayoutWithData } from './pageLayoutTypes'; + +export type DraftPageLayout = Omit< + PageLayoutWithData, + 'createdAt' | 'updatedAt' | 'deletedAt' +>; diff --git a/packages/twenty-front/src/modules/page-layout/types/page-layout-dragged-area.ts b/packages/twenty-front/src/modules/page-layout/types/page-layout-dragged-area.ts new file mode 100644 index 0000000000..a08bdc7974 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/types/page-layout-dragged-area.ts @@ -0,0 +1,6 @@ +export type PageLayoutDraggedArea = { + x: number; + y: number; + w: number; + h: number; +} | null; diff --git a/packages/twenty-front/src/modules/page-layout/types/tab-layouts.ts b/packages/twenty-front/src/modules/page-layout/types/tab-layouts.ts new file mode 100644 index 0000000000..fc8c947fa2 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/types/tab-layouts.ts @@ -0,0 +1,3 @@ +import { type Layouts } from 'react-grid-layout'; + +export type TabLayouts = Record; diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/createEmptyTabLayout.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getEmptyTabLayout.test.ts similarity index 74% rename from packages/twenty-front/src/modules/page-layout/utils/__tests__/createEmptyTabLayout.test.ts rename to packages/twenty-front/src/modules/page-layout/utils/__tests__/getEmptyTabLayout.test.ts index 675389d31f..b6d8809af6 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/createEmptyTabLayout.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getEmptyTabLayout.test.ts @@ -1,7 +1,7 @@ -import { type TabLayouts } from '../../states/pageLayoutCurrentLayoutsState'; -import { createEmptyTabLayout } from '../createEmptyTabLayout'; +import { type TabLayouts } from '@/page-layout/types/tab-layouts'; +import { getEmptyTabLayout } from '../getEmptyTabLayout'; -describe('createEmptyTabLayout', () => { +describe('getEmptyTabLayout', () => { const mockTabLayouts: TabLayouts = { 'tab-1': { desktop: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2 }], @@ -14,7 +14,7 @@ describe('createEmptyTabLayout', () => { }; it('should create empty layout for a new tab', () => { - const result = createEmptyTabLayout(mockTabLayouts, 'tab-3'); + const result = getEmptyTabLayout(mockTabLayouts, 'tab-3'); expect(result['tab-3']).toBeDefined(); expect(result['tab-3'].desktop).toEqual([]); @@ -22,7 +22,7 @@ describe('createEmptyTabLayout', () => { }); it('should preserve existing tabs', () => { - const result = createEmptyTabLayout(mockTabLayouts, 'tab-3'); + const result = getEmptyTabLayout(mockTabLayouts, 'tab-3'); expect(result['tab-1']).toEqual(mockTabLayouts['tab-1']); expect(result['tab-2']).toEqual(mockTabLayouts['tab-2']); @@ -30,7 +30,7 @@ describe('createEmptyTabLayout', () => { }); it('should overwrite existing tab with empty layout', () => { - const result = createEmptyTabLayout(mockTabLayouts, 'tab-1'); + const result = getEmptyTabLayout(mockTabLayouts, 'tab-1'); expect(result['tab-1'].desktop).toEqual([]); expect(result['tab-1'].mobile).toEqual([]); @@ -39,7 +39,7 @@ describe('createEmptyTabLayout', () => { it('should work with empty initial state', () => { const emptyLayouts: TabLayouts = {}; - const result = createEmptyTabLayout(emptyLayouts, 'tab-1'); + const result = getEmptyTabLayout(emptyLayouts, 'tab-1'); expect(result['tab-1']).toBeDefined(); expect(result['tab-1'].desktop).toEqual([]); @@ -49,7 +49,7 @@ describe('createEmptyTabLayout', () => { it('should return a new object without mutating the original', () => { const originalLayouts = JSON.parse(JSON.stringify(mockTabLayouts)); - const result = createEmptyTabLayout(mockTabLayouts, 'tab-3'); + const result = getEmptyTabLayout(mockTabLayouts, 'tab-3'); expect(result).not.toBe(mockTabLayouts); expect(mockTabLayouts).toEqual(originalLayouts); @@ -58,9 +58,9 @@ describe('createEmptyTabLayout', () => { }); it('should handle multiple new tabs', () => { - let result = createEmptyTabLayout(mockTabLayouts, 'tab-3'); - result = createEmptyTabLayout(result, 'tab-4'); - result = createEmptyTabLayout(result, 'tab-5'); + let result = getEmptyTabLayout(mockTabLayouts, 'tab-3'); + result = getEmptyTabLayout(result, 'tab-4'); + result = getEmptyTabLayout(result, 'tab-5'); expect(Object.keys(result)).toHaveLength(5); expect(result['tab-3'].desktop).toEqual([]); @@ -69,7 +69,7 @@ describe('createEmptyTabLayout', () => { }); it('should create consistent structure for desktop and mobile', () => { - const result = createEmptyTabLayout(mockTabLayouts, 'new-tab'); + const result = getEmptyTabLayout(mockTabLayouts, 'new-tab'); expect(result['new-tab']).toHaveProperty('desktop'); expect(result['new-tab']).toHaveProperty('mobile'); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/createUpdatedTabLayouts.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getUpdatedTabLayouts.test.ts similarity index 75% rename from packages/twenty-front/src/modules/page-layout/utils/__tests__/createUpdatedTabLayouts.test.ts rename to packages/twenty-front/src/modules/page-layout/utils/__tests__/getUpdatedTabLayouts.test.ts index 75f4e24c10..4d2cbdff9b 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/createUpdatedTabLayouts.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getUpdatedTabLayouts.test.ts @@ -1,7 +1,7 @@ -import { type TabLayouts } from '../../states/pageLayoutCurrentLayoutsState'; -import { createUpdatedTabLayouts } from '../createUpdatedTabLayouts'; +import { type TabLayouts } from '@/page-layout/types/tab-layouts'; +import { getUpdatedTabLayouts } from '../getUpdatedTabLayouts'; -describe('createUpdatedTabLayouts', () => { +describe('getUpdatedTabLayouts', () => { const mockTabLayouts: TabLayouts = { 'tab-1': { desktop: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2 }], @@ -16,7 +16,7 @@ describe('createUpdatedTabLayouts', () => { const newLayout = { i: 'widget-2', x: 2, y: 0, w: 3, h: 3 }; it('should add new layout to existing tab', () => { - const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout); + const result = getUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout); expect(result['tab-1'].desktop).toHaveLength(2); expect(result['tab-1'].desktop[1]).toEqual(newLayout); @@ -25,7 +25,7 @@ describe('createUpdatedTabLayouts', () => { }); it('should add layout to empty tab', () => { - const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-2', newLayout); + const result = getUpdatedTabLayouts(mockTabLayouts, 'tab-2', newLayout); expect(result['tab-2'].desktop).toHaveLength(1); expect(result['tab-2'].desktop[0]).toEqual(newLayout); @@ -34,7 +34,7 @@ describe('createUpdatedTabLayouts', () => { }); it('should create new tab entry if tab does not exist', () => { - const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-3', newLayout); + const result = getUpdatedTabLayouts(mockTabLayouts, 'tab-3', newLayout); expect(result['tab-3']).toBeDefined(); expect(result['tab-3'].desktop).toHaveLength(1); @@ -43,14 +43,14 @@ describe('createUpdatedTabLayouts', () => { }); it('should not modify other tabs', () => { - const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout); + const result = getUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout); expect(result['tab-2']).toEqual(mockTabLayouts['tab-2']); }); it('should handle mobile layout transformation correctly', () => { const wideLayout = { i: 'widget-3', x: 5, y: 2, w: 6, h: 4 }; - const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-1', wideLayout); + const result = getUpdatedTabLayouts(mockTabLayouts, 'tab-1', wideLayout); const mobileLayout = result['tab-1'].mobile[result['tab-1'].mobile.length - 1]; @@ -62,7 +62,7 @@ describe('createUpdatedTabLayouts', () => { it('should return a new object without mutating the original', () => { const originalLayouts = JSON.parse(JSON.stringify(mockTabLayouts)); - const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout); + const result = getUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout); expect(result).not.toBe(mockTabLayouts); expect(mockTabLayouts).toEqual(originalLayouts); @@ -76,7 +76,7 @@ describe('createUpdatedTabLayouts', () => { }; expect(() => { - createUpdatedTabLayouts(malformedLayouts, 'tab-1', newLayout); + getUpdatedTabLayouts(malformedLayouts, 'tab-1', newLayout); }).toThrow(); }); }); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/removeWidgetLayoutFromTab.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/removeWidgetLayoutFromTab.test.ts index 6b1d5b51e5..c843c91993 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/removeWidgetLayoutFromTab.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/removeWidgetLayoutFromTab.test.ts @@ -1,4 +1,4 @@ -import { type TabLayouts } from '../../states/pageLayoutCurrentLayoutsState'; +import { type TabLayouts } from '@/page-layout/types/tab-layouts'; import { removeWidgetLayoutFromTab } from '../removeWidgetLayoutFromTab'; describe('removeWidgetLayoutFromTab', () => { diff --git a/packages/twenty-front/src/modules/page-layout/utils/calculateGridCellPosition.ts b/packages/twenty-front/src/modules/page-layout/utils/calculateGridCellPosition.ts new file mode 100644 index 0000000000..48a8334510 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/calculateGridCellPosition.ts @@ -0,0 +1,12 @@ +export const calculateGridCellPosition = ({ + index, + numberOfColumns, +}: { + index: number; + numberOfColumns: number; +}) => { + const column = index % numberOfColumns; + const row = Math.floor(index / numberOfColumns); + + return { row, column }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/convertLayoutsToWidgets.ts b/packages/twenty-front/src/modules/page-layout/utils/convertLayoutsToWidgets.ts index 5943be2cdd..650406784b 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/convertLayoutsToWidgets.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/convertLayoutsToWidgets.ts @@ -9,6 +9,7 @@ export const convertLayoutsToWidgets = ( return widgets.map((widget) => { const layout = activeLayouts.find((l) => l.i === widget.id); + return { ...widget, gridPosition: { diff --git a/packages/twenty-front/src/modules/page-layout/utils/createEmptyTabLayout.ts b/packages/twenty-front/src/modules/page-layout/utils/getEmptyTabLayout.ts similarity index 56% rename from packages/twenty-front/src/modules/page-layout/utils/createEmptyTabLayout.ts rename to packages/twenty-front/src/modules/page-layout/utils/getEmptyTabLayout.ts index dd0e44a89c..7c88665a92 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/createEmptyTabLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/getEmptyTabLayout.ts @@ -1,6 +1,6 @@ -import { type TabLayouts } from '../states/pageLayoutCurrentLayoutsState'; +import { type TabLayouts } from '@/page-layout/types/tab-layouts'; -export const createEmptyTabLayout = ( +export const getEmptyTabLayout = ( allTabLayouts: TabLayouts, tabId: string, ): TabLayouts => { diff --git a/packages/twenty-front/src/modules/page-layout/utils/getTabListInstanceIdFromPageLayoutId.ts b/packages/twenty-front/src/modules/page-layout/utils/getTabListInstanceIdFromPageLayoutId.ts new file mode 100644 index 0000000000..ccafd1fdca --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/getTabListInstanceIdFromPageLayoutId.ts @@ -0,0 +1,3 @@ +export const getTabListInstanceIdFromPageLayoutId = (pageLayoutId: string) => { + return `${pageLayoutId}-tab-list`; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/createUpdatedTabLayouts.ts b/packages/twenty-front/src/modules/page-layout/utils/getUpdatedTabLayouts.ts similarity index 79% rename from packages/twenty-front/src/modules/page-layout/utils/createUpdatedTabLayouts.ts rename to packages/twenty-front/src/modules/page-layout/utils/getUpdatedTabLayouts.ts index 3654aebd51..5bc9cfe016 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/createUpdatedTabLayouts.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/getUpdatedTabLayouts.ts @@ -1,6 +1,6 @@ -import { type TabLayouts } from '../states/pageLayoutCurrentLayoutsState'; +import { type TabLayouts } from '@/page-layout/types/tab-layouts'; -export const createUpdatedTabLayouts = ( +export const getUpdatedTabLayouts = ( allTabLayouts: TabLayouts, activeTabId: string, newLayout: { i: string; x: number; y: number; w: number; h: number }, diff --git a/packages/twenty-front/src/modules/page-layout/utils/normalizePageLayoutData.ts b/packages/twenty-front/src/modules/page-layout/utils/normalizePageLayoutData.ts deleted file mode 100644 index c7e0b1ce80..0000000000 --- a/packages/twenty-front/src/modules/page-layout/utils/normalizePageLayoutData.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type PageLayout } from '~/generated/graphql'; -import { type PageLayoutWithData } from '../types/pageLayoutTypes'; - -export const normalizePageLayoutData = ( - pageLayout: PageLayout, -): PageLayoutWithData => { - return { - ...pageLayout, - tabs: (pageLayout.tabs || []).map((tab) => ({ - ...tab, - widgets: (tab.widgets || []).map((widget) => ({ - ...widget, - data: undefined, - })), - })), - }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/removeWidgetLayoutFromTab.ts b/packages/twenty-front/src/modules/page-layout/utils/removeWidgetLayoutFromTab.ts index c08d50b7c5..1e2040933f 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/removeWidgetLayoutFromTab.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/removeWidgetLayoutFromTab.ts @@ -1,4 +1,4 @@ -import { type TabLayouts } from '../states/pageLayoutCurrentLayoutsState'; +import { type TabLayouts } from '@/page-layout/types/tab-layouts'; export const removeWidgetLayoutFromTab = ( allTabLayouts: TabLayouts, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetHeader.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetHeader.tsx index f2d056193e..035e8ccf25 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetHeader.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetHeader.tsx @@ -28,7 +28,7 @@ const StyledTitle = styled.span` `; type WidgetHeaderProps = { - displayDragHandle: boolean; + isInEditMode: boolean; isEmpty?: boolean; title: string; onRemove?: () => void; @@ -36,15 +36,15 @@ type WidgetHeaderProps = { }; export const WidgetHeader = ({ - displayDragHandle = false, isEmpty = false, + isInEditMode = false, title, onEdit, onRemove, }: WidgetHeaderProps) => { return ( - {displayDragHandle && ( + {isInEditMode && ( )} {isEmpty ? 'Add Widget' : title} - {!isEmpty && onEdit && ( + {!isEmpty && isInEditMode && onEdit && ( )} - {!isEmpty && onRemove && ( + {!isEmpty && isInEditMode && onRemove && ( { return ( - + void; - onRemove?: () => void; }; const StyledContent = styled.div` @@ -19,19 +20,24 @@ const StyledContent = styled.div` justify-content: center; `; -export const WidgetRenderer = ({ - widget, - displayDragHandle = false, - onEdit, - onRemove, -}: WidgetRendererProps) => { +export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { + const { deletePageLayoutWidget } = useDeletePageLayoutWidget(); + const { handleEditWidget } = useEditPageLayoutWidget(); + + const isPageLayoutInEditMode = useRecoilComponentValue( + isPageLayoutInEditModeComponentState, + widget.id, + ); + return ( + handleEditWidget({ widgetId: widget.id, widgetType: widget.type }) + } + onRemove={() => deletePageLayoutWidget(widget.id)} /> diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx index a1d79b4e5c..bbe197e275 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx @@ -1,3 +1,4 @@ +import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper'; import { GraphType } from '@/page-layout/mocks/mockWidgets'; import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer'; import { type Meta, type StoryObj } from '@storybook/react'; @@ -7,7 +8,12 @@ import { WidgetType } from '~/generated/graphql'; const meta: Meta = { title: 'Modules/PageLayout/Widgets/WidgetRenderer', component: WidgetRenderer, - decorators: [ComponentDecorator], + decorators: [ + (Story, context) => ( + {Story(context)} + ), + ComponentDecorator, + ], parameters: { layout: 'centered', }, @@ -16,16 +22,6 @@ const meta: Meta = { control: 'object', description: 'Widget', }, - displayDragHandle: { - control: 'boolean', - description: 'Display drag handle', - }, - onRemove: { - action: 'onRemove', - }, - onEdit: { - action: 'onEdit', - }, }, }; @@ -55,16 +51,10 @@ export const WithNumberChart: Story = { trendPercentage: 12.5, }, }, - displayDragHandle: true, }, render: (args) => (
- +
), }; @@ -97,12 +87,7 @@ export const WithGaugeChart: Story = { }, render: (args) => (
- +
), }; @@ -163,12 +148,7 @@ export const WithPieChart: Story = { }, render: (args) => (
- +
), }; @@ -206,12 +186,7 @@ export const SmallWidget: Story = { }, render: (args) => (
- +
), }; @@ -251,12 +226,7 @@ export const MediumWidget: Story = { }, render: (args) => (
- +
), }; @@ -298,12 +268,7 @@ export const LargeWidget: Story = { }, render: (args) => (
- +
), }; @@ -341,12 +306,7 @@ export const WideWidget: Story = { }, render: (args) => (
- +
), }; @@ -386,12 +346,7 @@ export const TallWidget: Story = { }, render: (args) => (
- +
), }; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsPageFullWidthContainer.tsx b/packages/twenty-front/src/modules/settings/components/SettingsPageFullWidthContainer.tsx deleted file mode 100644 index 23069dcad7..0000000000 --- a/packages/twenty-front/src/modules/settings/components/SettingsPageFullWidthContainer.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { PageBody } from '@/ui/layout/page/components/PageBody'; -import { PageContainer } from '@/ui/layout/page/components/PageContainer'; -import { PageHeader } from '@/ui/layout/page/components/PageHeader'; -import { - Breadcrumb, - type BreadcrumbProps, -} from '@/ui/navigation/bread-crumb/components/Breadcrumb'; -import { type ReactNode } from 'react'; - -type SettingsPageFullWidthContainerProps = { - children: ReactNode; - links: BreadcrumbProps['links']; - actionButton?: ReactNode; -}; - -export const SettingsPageFullWidthContainer = ({ - children, - links, - actionButton, -}: SettingsPageFullWidthContainerProps) => { - return ( - - }> - {actionButton} - - {children} - - ); -}; diff --git a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx index 90418d8ddc..47e8bc91c8 100644 --- a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx +++ b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx @@ -20,7 +20,6 @@ import { IconFunction, IconHierarchy2, IconKey, - IconLayout, IconLock, IconMail, IconRocket, @@ -64,9 +63,6 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { (currentUser?.canImpersonate || currentUser?.canAccessFullAdminPanel) ?? false; const isAIEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED); - const isPageLayoutEnabled = useIsFeatureEnabled( - FeatureFlagKey.IS_PAGE_LAYOUT_ENABLED, - ); const permissionMap = usePermissionFlagMap(); return [ @@ -144,15 +140,6 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { isHidden: !isBillingEnabled || !permissionMap[PermissionFlagType.WORKSPACE], }, - { - label: t`Page Layouts`, - path: SettingsPath.PageLayout, - Icon: IconLayout, - isHidden: - !isPageLayoutEnabled || - !permissionMap[PermissionFlagType.WORKSPACE], - isNew: true, - }, { label: t`APIs & Webhooks`, path: SettingsPath.ApiWebhooks, diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx index b470b7f3e4..f8a436a53d 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx @@ -17,9 +17,7 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { Global, css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { AnimatePresence, LayoutGroup, motion } from 'framer-motion'; -import { Outlet, useLocation } from 'react-router-dom'; -import { SettingsPath } from 'twenty-shared/types'; -import { getSettingsPath } from 'twenty-shared/utils'; +import { Outlet } from 'react-router-dom'; import { useScreenSize } from 'twenty-ui/utilities'; const StyledLayout = styled.div` @@ -61,12 +59,6 @@ const StyledMainContainer = styled.div` export const DefaultLayout = () => { const isMobile = useIsMobile(); const isSettingsPage = useIsSettingsPage(); - const location = useLocation(); - const isPageLayoutEditor = - location.pathname.includes(getSettingsPath(SettingsPath.PageLayoutNew)) || - location.pathname.match( - new RegExp(`${getSettingsPath(SettingsPath.PageLayout)}/[^/]+$`), - ); const theme = useTheme(); const windowsWidth = useScreenSize().width; const showAuthModal = useShowAuthModal(); @@ -86,10 +78,7 @@ export const DefaultLayout = () => { theme.background.primary}; - box-sizing: border-box; - flex: 1; - min-height: 100%; - overflow-y: auto; - overflow-x: hidden; - position: relative; - padding: ${({ theme }) => theme.spacing(2)}; - width: 100%; - user-select: none; - - .react-grid-placeholder { - background: ${({ theme }) => theme.adaptiveColors.blue3} !important; - - border-radius: ${({ theme }) => theme.border.radius.sm}; - } - - .react-grid-item:not(.react-draggable-dragging) { - user-select: auto; - } -`; - -const StyledTabList = styled(TabList)` - padding-left: ${({ theme }) => theme.spacing(2)}; -`; - -const StyledGridOverlay = styled.div<{ - isDragSelecting?: boolean; - breakpoint: PageLayoutBreakpoint; -}>` - position: absolute; - top: ${({ theme }) => theme.spacing(2)}; - left: ${({ theme }) => theme.spacing(2)}; - right: ${({ theme }) => theme.spacing(2)}; - bottom: ${({ theme }) => theme.spacing(2)}; - display: grid; - grid-template-columns: ${({ breakpoint }) => - breakpoint === 'mobile' ? '1fr' : 'repeat(12, 1fr)'}; - grid-auto-rows: 55px; - gap: ${({ theme }) => theme.spacing(2)}; - pointer-events: ${({ isDragSelecting }) => - isDragSelecting ? 'auto' : 'none'}; - z-index: 0; -`; - -const StyledActionButtonContainer = styled.div` - align-items: center; - display: flex; - gap: ${({ theme }) => theme.spacing(2)}; -`; - -const StyledGridCell = styled.div<{ isSelected?: boolean }>` - background: ${({ isSelected, theme }) => - isSelected ? theme.adaptiveColors.blue1 : 'transparent'}; - border: 1px solid - ${({ theme, isSelected }) => - isSelected ? theme.adaptiveColors.blue3 : theme.border.color.light}; - border-radius: ${({ theme }) => theme.border.radius.md}; - transition: background-color 0.3s ease; - - &:hover { - background: ${({ theme }) => theme.background.transparent.lighter}; - border-color: ${({ theme }) => theme.border.color.medium}; - } -`; - -type ExtendedResponsiveProps = ResponsiveProps & { - maxCols?: number; - preventCollision?: boolean; -}; - -const ResponsiveGridLayout = WidthProvider( - Responsive, -) as React.ComponentType; - -export const SettingsPageLayoutEdit = () => { - const { t } = useLingui(); - const { id } = useParams<{ id: string }>(); - const isEditMode = id && id !== 'new'; - - const { pageLayoutDraft, setPageLayoutDraft, isDirty } = - usePageLayoutDraftState(); - - const { savePageLayout } = usePageLayoutSaveHandler(); - const navigateSettings = useNavigateSettings(); - const [isSaving, setIsSaving] = useState(false); - - const [pageLayoutCurrentBreakpoint, setPageLayoutCurrentBreakpoint] = - useRecoilState(pageLayoutCurrentBreakpointState); - const pageLayoutSelectedCells = useRecoilValue(pageLayoutSelectedCellsState); - const pageLayoutCurrentLayouts = useRecoilValue( - pageLayoutCurrentLayoutsState, - ); - const { navigateCommandMenu } = useNavigateCommandMenu(); - const setPageLayoutEditingWidgetId = useSetRecoilState( - pageLayoutEditingWidgetIdState, - ); - - const gridContainerRef = useRef(null); - - const activeTabId = useRecoilComponentValue( - activeTabIdComponentState, - SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID, - ); - - const setActiveTabId = useSetRecoilState( - activeTabIdComponentState.atomFamily({ - instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID, - }), - ); - - const activeTabWidgets = useMemo(() => { - if (!activeTabId) return []; - const activeTab = pageLayoutDraft.tabs.find( - (tab) => tab.id === activeTabId, - ); - return activeTab?.widgets ?? []; - }, [pageLayoutDraft.tabs, activeTabId]); - - const allWidgets = useMemo( - () => pageLayoutDraft.tabs.flatMap((tab) => tab.widgets).filter(isDefined), - [pageLayoutDraft.tabs], - ); - - const { startPageLayoutDragSelection } = useStartPageLayoutDragSelection(); - const { changePageLayoutDragSelection } = useChangePageLayoutDragSelection(); - const { endPageLayoutDragSelection } = useEndPageLayoutDragSelection(); - - const handleOpenAddWidget = useCallback(() => { - navigateCommandMenu({ - page: CommandMenuPages.PageLayoutWidgetTypeSelect, - pageTitle: 'Add Widget', - pageIcon: IconAppWindow, - resetNavigationStack: true, - }); - }, [navigateCommandMenu]); - - const { deletePageLayoutWidget } = useDeletePageLayoutWidget(); - const { handleLayoutChange } = usePageLayoutHandleLayoutChange(activeTabId); - const { createPageLayoutTab } = useCreatePageLayoutTab(); - - const handleEditWidget = useCallback( - (widgetId: string) => { - const widget = allWidgets.find((w) => w.id === widgetId); - if (!widget) return; - - setPageLayoutEditingWidgetId(widgetId); - - if (widget.type === WidgetType.IFRAME) { - navigateCommandMenu({ - page: CommandMenuPages.PageLayoutIframeConfig, - pageTitle: 'Edit iFrame', - pageIcon: IconAppWindow, - resetNavigationStack: true, - }); - } - }, - [allWidgets, setPageLayoutEditingWidgetId, navigateCommandMenu], - ); - - const isEmptyState = activeTabWidgets.length === 0; - - const gridRows = useMemo(() => { - const currentTabLayouts = pageLayoutCurrentLayouts[activeTabId || ''] || { - desktop: [], - mobile: [], - }; - return calculateTotalGridRows(currentTabLayouts); - }, [pageLayoutCurrentLayouts, activeTabId]); - - const handleCancel = () => { - navigateSettings(SettingsPath.PageLayout); - }; - - const handleAddTab = useCallback(() => { - const newTabId = createPageLayoutTab(); - setActiveTabId(newTabId); - }, [createPageLayoutTab, setActiveTabId]); - - const tabListTabs: SingleTabProps[] = useMemo(() => { - return [...(pageLayoutDraft.tabs ?? [])] - .sort((a, b) => a.position - b.position) - .map((tab) => ({ - id: tab.id, - title: tab.title, - })); - }, [pageLayoutDraft.tabs]); - - const handleSaveClick = async () => { - setIsSaving(true); - try { - const allWidgets: PageLayoutWidgetWithData[] = - pageLayoutDraft.tabs.flatMap((tab) => - tab.widgets.map((widget) => ({ - ...widget, - pageLayoutTabId: widget.pageLayoutTabId ?? tab.id, - objectMetadataId: widget.objectMetadataId || null, - createdAt: widget.createdAt ?? new Date().toISOString(), - updatedAt: new Date().toISOString(), - deletedAt: widget.deletedAt ?? null, - })), - ); - - setPageLayoutDraft((prev) => ({ - ...prev, - tabs: pageLayoutDraft.tabs, - })); - - await savePageLayout(allWidgets); - } finally { - setIsSaving(false); - } - }; - - return ( - <> - - - setPageLayoutDraft((prev) => ({ ...prev, name: value })) - } - sizeVariant="md" - /> - ), - }, - ]} - actionButton={ - - - {!isEmptyState && ( -