Page layout refactoring (#14535)
In this PR: - Refactored the page layout renderer - Converted all the states to component states - Removed the page layout edition in settings TODOs in next PRs: - Fix bug with the drag selector not taking the scroll into account to display the dragged area - Readd the tab edition in edit mode
This commit is contained in:
@@ -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 (
|
||||
<RouterProvider
|
||||
router={useCreateAppRouter(
|
||||
isFunctionSettingsEnabled,
|
||||
isAdminPageEnabled,
|
||||
isPageLayoutFeatureFlagEnabled,
|
||||
)}
|
||||
router={useCreateAppRouter(isFunctionSettingsEnabled, isAdminPageEnabled)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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) => (
|
||||
<Suspense fallback={<SettingsSkeletonLoader />}>
|
||||
<Routes>
|
||||
@@ -614,22 +598,7 @@ export const SettingsRoutes = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isPageLayoutFeatureFlagEnabled && (
|
||||
<>
|
||||
<Route
|
||||
path={SettingsPath.PageLayout}
|
||||
element={<SettingsPageLayouts />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.PageLayoutNew}
|
||||
element={<SettingsPageLayoutEdit />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.PageLayoutEdit}
|
||||
element={<SettingsPageLayoutEdit />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Route
|
||||
element={
|
||||
<SettingsProtectedRouteWrapper
|
||||
|
||||
@@ -31,7 +31,6 @@ import { SyncEmails } from '~/pages/onboarding/SyncEmails';
|
||||
export const useCreateAppRouter = (
|
||||
isFunctionSettingsEnabled?: boolean,
|
||||
isAdminPageEnabled?: boolean,
|
||||
isPageLayoutFeatureFlagEnabled?: boolean,
|
||||
) =>
|
||||
createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
@@ -70,7 +69,6 @@ export const useCreateAppRouter = (
|
||||
<SettingsRoutes
|
||||
isFunctionSettingsEnabled={isFunctionSettingsEnabled}
|
||||
isAdminPageEnabled={isAdminPageEnabled}
|
||||
isPageLayoutFeatureFlagEnabled={isPageLayoutFeatureFlagEnabled}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<CommandMenuContainer>
|
||||
<CommandMenuContextChipRecordSetterEffect />
|
||||
@@ -64,7 +51,6 @@ export const CommandMenuRouter = () => {
|
||||
isInRightDrawer={true}
|
||||
displayType="listItem"
|
||||
actionMenuType="command-menu"
|
||||
objectMetadataItemOverride={objectMetadataItemOverride}
|
||||
>
|
||||
{commandMenuPageComponent}
|
||||
</ActionMenuContextProvider>
|
||||
|
||||
+5
-1
@@ -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);
|
||||
|
||||
+26
-7
@@ -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());
|
||||
|
||||
+21
-16
@@ -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;
|
||||
|
||||
+42
@@ -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,
|
||||
};
|
||||
};
|
||||
+25
@@ -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 };
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
|
||||
export type PageLayoutCommandMenuPage =
|
||||
| CommandMenuPages.PageLayoutWidgetTypeSelect
|
||||
| CommandMenuPages.PageLayoutGraphTypeSelect
|
||||
| CommandMenuPages.PageLayoutIframeConfig;
|
||||
+17
@@ -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);
|
||||
}
|
||||
};
|
||||
+17
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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<Dashboard>({
|
||||
objectNameSingular: CoreObjectNameSingular.Dashboard,
|
||||
objectRecordId: recordId,
|
||||
});
|
||||
|
||||
const { data } = useQuery(FIND_ONE_PAGE_LAYOUT, {
|
||||
variables: {
|
||||
id: dashboard?.pageLayoutId,
|
||||
},
|
||||
skip: dashboardLoading || !dashboard?.pageLayoutId,
|
||||
const { record: dashboard } = useFindOneRecord<Dashboard>({
|
||||
objectNameSingular: CoreObjectNameSingular.Dashboard,
|
||||
objectRecordId: recordId,
|
||||
});
|
||||
|
||||
const pageLayout = data?.getPageLayout;
|
||||
|
||||
if (!pageLayout) {
|
||||
if (!isDefined(dashboard)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedPageLayout = normalizePageLayoutData(pageLayout);
|
||||
|
||||
return <PageLayoutRenderer pageLayout={normalizedPageLayout} />;
|
||||
return <PageLayoutRenderer pageLayoutId={dashboard.pageLayoutId} />;
|
||||
};
|
||||
|
||||
+1
@@ -10,5 +10,6 @@ export const PAGE_LAYOUT_TAB_FRAGMENT = gql`
|
||||
widgets {
|
||||
...PageLayoutWidgetFragment
|
||||
}
|
||||
pageLayoutId
|
||||
}
|
||||
`;
|
||||
|
||||
+1
@@ -13,5 +13,6 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
rowSpan
|
||||
}
|
||||
configuration
|
||||
pageLayoutTabId
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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<ExtendedResponsiveProps>;
|
||||
|
||||
export const PageLayoutGridLayout = () => {
|
||||
const setPageLayoutCurrentBreakpoint = useSetRecoilComponentState(
|
||||
pageLayoutCurrentBreakpointComponentState,
|
||||
);
|
||||
|
||||
const { handleLayoutChange } = usePageLayoutHandleLayoutChange();
|
||||
|
||||
const gridContainerRef = useRef<HTMLDivElement>(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 (
|
||||
<>
|
||||
<StyledGridContainer ref={gridContainerRef}>
|
||||
{isPageLayoutInEditMode && (
|
||||
<>
|
||||
<PageLayoutGridOverlay />
|
||||
<PageLayoutGridLayoutDragSelector
|
||||
gridContainerRef={gridContainerRef}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ResponsiveGridLayout
|
||||
className="layout"
|
||||
layouts={layouts}
|
||||
breakpoints={PAGE_LAYOUT_CONFIG.breakpoints}
|
||||
cols={PAGE_LAYOUT_CONFIG.columns}
|
||||
rowHeight={55}
|
||||
maxCols={12}
|
||||
containerPadding={[0, 0]}
|
||||
margin={[8, 8]}
|
||||
isDraggable={isPageLayoutInEditMode}
|
||||
isResizable={isPageLayoutInEditMode}
|
||||
draggableHandle=".drag-handle"
|
||||
compactType="vertical"
|
||||
preventCollision={false}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
onBreakpointChange={(newBreakpoint) =>
|
||||
setPageLayoutCurrentBreakpoint(
|
||||
newBreakpoint as PageLayoutBreakpoint,
|
||||
)
|
||||
}
|
||||
>
|
||||
{isLayoutEmpty ? (
|
||||
<div key="empty-placeholder" data-select-disable="true">
|
||||
<WidgetPlaceholder
|
||||
onClick={() => {
|
||||
navigatePageLayoutCommandMenu({
|
||||
commandMenuPage:
|
||||
CommandMenuPages.PageLayoutWidgetTypeSelect,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
activeTabWidgets?.map((widget) => (
|
||||
<div key={widget.id} data-select-disable="true">
|
||||
<WidgetRenderer widget={widget as Widget} />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</ResponsiveGridLayout>
|
||||
</StyledGridContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+24
@@ -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<HTMLDivElement>;
|
||||
}) => {
|
||||
const { startPageLayoutDragSelection } = useStartPageLayoutDragSelection();
|
||||
const { changePageLayoutDragSelection } = useChangePageLayoutDragSelection();
|
||||
const { endPageLayoutDragSelection } = useEndPageLayoutDragSelection();
|
||||
|
||||
return (
|
||||
<DragSelect
|
||||
selectableItemsContainerRef={gridContainerRef}
|
||||
onDragSelectionStart={startPageLayoutDragSelection}
|
||||
onDragSelectionChange={changePageLayoutDragSelection}
|
||||
onDragSelectionEnd={endPageLayoutDragSelection}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<StyledGridOverlay
|
||||
isDragSelecting={!isPageLayoutCurrentBreakpointMobile}
|
||||
breakpoint={pageLayoutCurrentBreakpoint}
|
||||
>
|
||||
{Array.from(
|
||||
{
|
||||
length: numberOfColumns * numberOfRows,
|
||||
},
|
||||
(_, i) => {
|
||||
const { column, row } = calculateGridCellPosition({
|
||||
index: i,
|
||||
numberOfColumns,
|
||||
});
|
||||
const cellId = generateCellId(column, row);
|
||||
return (
|
||||
<StyledGridCell
|
||||
key={i}
|
||||
data-selectable-id={cellId}
|
||||
isSelected={pageLayoutSelectedCells.has(cellId)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</StyledGridOverlay>
|
||||
);
|
||||
};
|
||||
-117
@@ -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;
|
||||
};
|
||||
+96
@@ -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;
|
||||
};
|
||||
@@ -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<ExtendedResponsiveProps>;
|
||||
|
||||
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 (
|
||||
<>
|
||||
<PageLayoutInitializationEffect
|
||||
layoutId={pageLayout.id}
|
||||
isEditMode={false}
|
||||
pageLayout={pageLayout}
|
||||
/>
|
||||
{pageLayout.tabs.length > 0 && (
|
||||
<StyledTabList
|
||||
tabs={tabListTabs}
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={pageLayout.id}
|
||||
/>
|
||||
)}
|
||||
<StyledGridContainer>
|
||||
<ResponsiveGridLayout
|
||||
className="layout"
|
||||
layouts={
|
||||
!activeTabId
|
||||
? EMPTY_LAYOUT
|
||||
: pageLayoutCurrentLayouts[activeTabId] || EMPTY_LAYOUT
|
||||
}
|
||||
breakpoints={PAGE_LAYOUT_CONFIG.breakpoints}
|
||||
cols={PAGE_LAYOUT_CONFIG.columns}
|
||||
rowHeight={55}
|
||||
maxCols={12}
|
||||
containerPadding={[0, 0]}
|
||||
margin={[8, 8]}
|
||||
isDraggable={false}
|
||||
isResizable={false}
|
||||
draggableHandle=".drag-handle"
|
||||
compactType="vertical"
|
||||
preventCollision={false}
|
||||
onBreakpointChange={(newBreakpoint) =>
|
||||
setPageLayoutCurrentBreakpoint(
|
||||
newBreakpoint as PageLayoutBreakpoint,
|
||||
)
|
||||
}
|
||||
>
|
||||
{activeTabWidgets?.map((widget) => (
|
||||
<div key={widget.id} data-select-disable="true">
|
||||
<WidgetRenderer widget={widget as Widget} />
|
||||
</div>
|
||||
))}
|
||||
</ResponsiveGridLayout>
|
||||
</StyledGridContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageLayoutRenderer = ({ pageLayout }: PageLayoutRendererProps) => {
|
||||
return (
|
||||
<TabListComponentInstanceContext.Provider
|
||||
value={{ instanceId: pageLayout.id }}
|
||||
<PageLayoutComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: pageLayoutId,
|
||||
}}
|
||||
>
|
||||
<PageLayoutRendererContent pageLayout={pageLayout} />
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
<TabListComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: getTabListInstanceIdFromPageLayoutId(pageLayoutId),
|
||||
}}
|
||||
>
|
||||
<PageLayoutInitializationQueryEffect pageLayoutId={pageLayoutId} />
|
||||
<PageLayoutRendererContent />
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
</PageLayoutComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+31
@@ -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 (
|
||||
<>
|
||||
<StyledTabList
|
||||
tabs={currentPageLayout.tabs}
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={getTabListInstanceIdFromPageLayoutId(
|
||||
currentPageLayout.id,
|
||||
)}
|
||||
/>
|
||||
<PageLayoutGridLayout />
|
||||
</>
|
||||
);
|
||||
};
|
||||
+38
-40
@@ -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<typeof PageLayoutRenderer> = {
|
||||
title: 'Modules/PageLayout/PageLayoutRenderer',
|
||||
component: PageLayoutRenderer,
|
||||
decorators: [
|
||||
(Story, { args }: { args: any }) => (
|
||||
(Story) => (
|
||||
<MemoryRouter>
|
||||
<RecoilRoot
|
||||
initializeState={({ set }) => {
|
||||
set(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: 'page-layout-stories',
|
||||
}),
|
||||
args.activeTabId,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<TabListComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'page-layout-stories' }}
|
||||
>
|
||||
<MockedProvider mocks={graphqlMocks} addTypename={false}>
|
||||
<RecoilRoot>
|
||||
<Story />
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
</RecoilRoot>
|
||||
</RecoilRoot>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
),
|
||||
],
|
||||
@@ -190,7 +194,7 @@ const meta: Meta<typeof PageLayoutRenderer> = {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
args: {
|
||||
pageLayout: mixedGraphsPageLayout,
|
||||
pageLayoutId: mixedGraphsPageLayoutMocks.id,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -199,9 +203,6 @@ export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
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',
|
||||
|
||||
+32
@@ -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 (
|
||||
<PageLayoutComponentInstanceContext.Provider value={{ instanceId }}>
|
||||
<TabListComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: getTabListInstanceIdFromPageLayoutId(instanceId),
|
||||
}}
|
||||
>
|
||||
<RecoilRoot initializeState={initializeState}>{children}</RecoilRoot>
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
</PageLayoutComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
-139
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
+191
@@ -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 }) => (
|
||||
<PageLayoutTestWrapper
|
||||
initializeState={({ set }) => {
|
||||
set(
|
||||
pageLayoutSelectedCellsComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
}),
|
||||
new Set(['cell-1']),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<PageLayoutTestWrapper
|
||||
initializeState={({ set }) => {
|
||||
set(
|
||||
pageLayoutSelectedCellsComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
}),
|
||||
new Set(['cell-1', 'cell-2']),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<PageLayoutTestWrapper>{children}</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<PageLayoutTestWrapper
|
||||
initializeState={({ set }) => {
|
||||
set(
|
||||
pageLayoutSelectedCellsComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
}),
|
||||
new Set(['cell-1']),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<PageLayoutTestWrapper>{children}</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
expect(typeof result.current.changePageLayoutDragSelection).toBe(
|
||||
'function',
|
||||
);
|
||||
});
|
||||
});
|
||||
+35
-16
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
+57
-26
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
+22
-10
@@ -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');
|
||||
-208
@@ -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);
|
||||
});
|
||||
});
|
||||
+271
@@ -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<string>, 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 }) => (
|
||||
<PageLayoutTestWrapper
|
||||
initializeState={createInitializeState(
|
||||
new Set(['0-0', '0-1', '1-0', '1-1']),
|
||||
null,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<PageLayoutTestWrapper
|
||||
initializeState={createInitializeState(new Set(), null)}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<PageLayoutTestWrapper
|
||||
initializeState={createInitializeState(
|
||||
new Set(['invalid-cell']),
|
||||
null,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<PageLayoutTestWrapper>{children}</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<PageLayoutTestWrapper
|
||||
initializeState={createInitializeState(new Set(['0-0']))}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<PageLayoutTestWrapper
|
||||
initializeState={createInitializeState(new Set(['0-0']))}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.selectedCells.size).toBe(1);
|
||||
|
||||
act(() => {
|
||||
result.current.endDragSelection.endPageLayoutDragSelection();
|
||||
});
|
||||
|
||||
expect(result.current.selectedCells.size).toBe(0);
|
||||
});
|
||||
});
|
||||
+31
-13
@@ -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,
|
||||
-95
@@ -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);
|
||||
});
|
||||
});
|
||||
+124
@@ -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 }) => (
|
||||
<PageLayoutTestWrapper
|
||||
initializeState={({ set }) => {
|
||||
set(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: `${PAGE_LAYOUT_TEST_INSTANCE_ID}-tab-list`,
|
||||
}),
|
||||
'tab-1',
|
||||
);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<PageLayoutTestWrapper
|
||||
initializeState={({ set }) => {
|
||||
set(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: `${PAGE_LAYOUT_TEST_INSTANCE_ID}-tab-list`,
|
||||
}),
|
||||
'tab-1',
|
||||
);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
-73
@@ -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);
|
||||
});
|
||||
});
|
||||
+108
@@ -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<string>) =>
|
||||
({ 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 }) => (
|
||||
<PageLayoutTestWrapper
|
||||
initializeState={createInitializeState(
|
||||
new Set(['cell-1', 'cell-2']),
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<PageLayoutTestWrapper>{children}</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<PageLayoutTestWrapper
|
||||
initializeState={createInitializeState(new Set(['cell-1']))}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
+18
-3
@@ -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 };
|
||||
|
||||
+41
-9
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
+33
-14
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
|
||||
+39
-6
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
+18
-3
@@ -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 };
|
||||
|
||||
@@ -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<PageLayoutWidgetWithData>) => {
|
||||
@@ -16,7 +29,7 @@ export const useUpdatePageLayoutWidget = () => {
|
||||
})),
|
||||
}));
|
||||
},
|
||||
[],
|
||||
[pageLayoutDraftState],
|
||||
);
|
||||
|
||||
return { updatePageLayoutWidget };
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const PageLayoutComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
|
||||
|
||||
export const isPageLayoutInEditModeComponentState =
|
||||
createComponentState<boolean>({
|
||||
key: 'isPageLayoutInEditModeComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
+11
@@ -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<PageLayoutBreakpoint>({
|
||||
key: 'pageLayoutCurrentBreakpointComponentState',
|
||||
defaultValue: 'desktop',
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
import { type PageLayoutBreakpoint } from '../constants/PageLayoutBreakpoints';
|
||||
|
||||
export const pageLayoutCurrentBreakpointState =
|
||||
createState<PageLayoutBreakpoint>({
|
||||
key: 'pageLayoutCurrentBreakpointState',
|
||||
defaultValue: 'desktop',
|
||||
});
|
||||
+11
@@ -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<TabLayouts>({
|
||||
key: 'pageLayoutCurrentLayoutsComponentState',
|
||||
defaultValue: {},
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export type TabLayouts = Record<string, Layouts>;
|
||||
|
||||
export const pageLayoutCurrentLayoutsState = createState<TabLayouts>({
|
||||
key: 'pageLayoutCurrentLayoutsState',
|
||||
defaultValue: {},
|
||||
});
|
||||
@@ -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<DraftPageLayout>({
|
||||
key: 'pageLayoutDraftComponentState',
|
||||
defaultValue: {
|
||||
id: '',
|
||||
name: '',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
tabs: [],
|
||||
},
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
@@ -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<DraftPageLayout>({
|
||||
key: 'pageLayoutDraftState',
|
||||
defaultValue: {
|
||||
name: '',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
tabs: [],
|
||||
},
|
||||
});
|
||||
+11
@@ -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<PageLayoutDraggedArea>({
|
||||
key: 'pageLayoutDraggedAreaComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
@@ -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<DraggedArea>({
|
||||
key: 'pageLayoutDraggedAreaState',
|
||||
defaultValue: null,
|
||||
});
|
||||
+11
@@ -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,
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const pageLayoutEditingWidgetIdState = createState<string | null>({
|
||||
key: 'pageLayoutEditingWidgetIdState',
|
||||
defaultValue: null,
|
||||
});
|
||||
+12
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
|
||||
|
||||
export const pageLayoutSelectedCellsComponentState = createComponentState<
|
||||
Set<string>
|
||||
>({
|
||||
key: 'pageLayoutSelectedCellsComponentState',
|
||||
defaultValue: new Set(),
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const pageLayoutSelectedCellsState = createState<Set<string>>({
|
||||
key: 'pageLayoutSelectedCellsState',
|
||||
defaultValue: new Set(),
|
||||
});
|
||||
+12
@@ -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,
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
import { type PageLayoutWithData } from '../types/pageLayoutTypes';
|
||||
|
||||
export const savedPageLayoutsState = createState<PageLayoutWithData[]>({
|
||||
key: 'savedPageLayoutsState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type PageLayoutWithData } from './pageLayoutTypes';
|
||||
|
||||
export type DraftPageLayout = Omit<
|
||||
PageLayoutWithData,
|
||||
'createdAt' | 'updatedAt' | 'deletedAt'
|
||||
>;
|
||||
@@ -0,0 +1,6 @@
|
||||
export type PageLayoutDraggedArea = {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
} | null;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
|
||||
export type TabLayouts = Record<string, Layouts>;
|
||||
+12
-12
@@ -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');
|
||||
+10
-10
@@ -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();
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -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', () => {
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -9,6 +9,7 @@ export const convertLayoutsToWidgets = (
|
||||
|
||||
return widgets.map((widget) => {
|
||||
const layout = activeLayouts.find((l) => l.i === widget.id);
|
||||
|
||||
return {
|
||||
...widget,
|
||||
gridPosition: {
|
||||
|
||||
+2
-2
@@ -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 => {
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export const getTabListInstanceIdFromPageLayoutId = (pageLayoutId: string) => {
|
||||
return `${pageLayoutId}-tab-list`;
|
||||
};
|
||||
+2
-2
@@ -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 },
|
||||
@@ -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,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type TabLayouts } from '../states/pageLayoutCurrentLayoutsState';
|
||||
import { type TabLayouts } from '@/page-layout/types/tab-layouts';
|
||||
|
||||
export const removeWidgetLayoutFromTab = (
|
||||
allTabLayouts: TabLayouts,
|
||||
|
||||
@@ -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 (
|
||||
<StyledHeader>
|
||||
{displayDragHandle && (
|
||||
{isInEditMode && (
|
||||
<StyledDragHandleButton
|
||||
Icon={IconGripVertical}
|
||||
className="drag-handle"
|
||||
@@ -54,7 +54,7 @@ export const WidgetHeader = ({
|
||||
/>
|
||||
)}
|
||||
<StyledTitle>{isEmpty ? 'Add Widget' : title}</StyledTitle>
|
||||
{!isEmpty && onEdit && (
|
||||
{!isEmpty && isInEditMode && onEdit && (
|
||||
<IconButton
|
||||
onClick={onEdit}
|
||||
Icon={IconPencil}
|
||||
@@ -62,7 +62,7 @@ export const WidgetHeader = ({
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
{!isEmpty && onRemove && (
|
||||
{!isEmpty && isInEditMode && onRemove && (
|
||||
<IconButton
|
||||
onClick={onRemove}
|
||||
Icon={IconX}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ type WidgetPlaceholderProps = {
|
||||
export const WidgetPlaceholder = ({ onClick }: WidgetPlaceholderProps) => {
|
||||
return (
|
||||
<WidgetContainer onClick={onClick}>
|
||||
<WidgetHeader displayDragHandle={true} title="Add Widget" isEmpty />
|
||||
<WidgetHeader isInEditMode={true} title="Add Widget" isEmpty />
|
||||
<AnimatedPlaceholderEmptyContainer
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
|
||||
|
||||
+18
-12
@@ -1,14 +1,15 @@
|
||||
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { useEditPageLayoutWidget } from '@/page-layout/hooks/useEditPageLayoutWidget';
|
||||
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
|
||||
import { WidgetContainer } from '@/page-layout/widgets/components/WidgetContainer';
|
||||
import { WidgetContentRenderer } from '@/page-layout/widgets/components/WidgetContentRenderer';
|
||||
import { WidgetHeader } from '@/page-layout/widgets/components/WidgetHeader';
|
||||
import { type Widget as WidgetType } from '@/page-layout/widgets/types/Widget';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type WidgetRendererProps = {
|
||||
widget: WidgetType;
|
||||
displayDragHandle?: boolean;
|
||||
onEdit?: () => 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 (
|
||||
<WidgetContainer>
|
||||
<WidgetHeader
|
||||
displayDragHandle={displayDragHandle}
|
||||
isInEditMode={isPageLayoutInEditMode}
|
||||
title={widget.title}
|
||||
onEdit={onEdit}
|
||||
onRemove={onRemove}
|
||||
onEdit={() =>
|
||||
handleEditWidget({ widgetId: widget.id, widgetType: widget.type })
|
||||
}
|
||||
onRemove={() => deletePageLayoutWidget(widget.id)}
|
||||
/>
|
||||
<StyledContent>
|
||||
<WidgetContentRenderer widget={widget} />
|
||||
|
||||
+15
-60
@@ -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<typeof WidgetRenderer> = {
|
||||
title: 'Modules/PageLayout/Widgets/WidgetRenderer',
|
||||
component: WidgetRenderer,
|
||||
decorators: [ComponentDecorator],
|
||||
decorators: [
|
||||
(Story, context) => (
|
||||
<PageLayoutTestWrapper>{Story(context)}</PageLayoutTestWrapper>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
@@ -16,16 +22,6 @@ const meta: Meta<typeof WidgetRenderer> = {
|
||||
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) => (
|
||||
<div style={{ width: '300px', height: '100px' }}>
|
||||
<WidgetRenderer
|
||||
widget={args.widget}
|
||||
displayDragHandle={args.displayDragHandle}
|
||||
onRemove={args.onRemove}
|
||||
onEdit={args.onEdit}
|
||||
/>
|
||||
<WidgetRenderer widget={args.widget} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -97,12 +87,7 @@ export const WithGaugeChart: Story = {
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '300px', height: '400px' }}>
|
||||
<WidgetRenderer
|
||||
widget={args.widget}
|
||||
displayDragHandle={args.displayDragHandle}
|
||||
onRemove={args.onRemove}
|
||||
onEdit={args.onEdit}
|
||||
/>
|
||||
<WidgetRenderer widget={args.widget} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -163,12 +148,7 @@ export const WithPieChart: Story = {
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '300px', height: '500px' }}>
|
||||
<WidgetRenderer
|
||||
widget={args.widget}
|
||||
displayDragHandle={args.displayDragHandle}
|
||||
onRemove={args.onRemove}
|
||||
onEdit={args.onEdit}
|
||||
/>
|
||||
<WidgetRenderer widget={args.widget} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -206,12 +186,7 @@ export const SmallWidget: Story = {
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '300px', height: '100px' }}>
|
||||
<WidgetRenderer
|
||||
widget={args.widget}
|
||||
displayDragHandle={args.displayDragHandle}
|
||||
onRemove={args.onRemove}
|
||||
onEdit={args.onEdit}
|
||||
/>
|
||||
<WidgetRenderer widget={args.widget} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -251,12 +226,7 @@ export const MediumWidget: Story = {
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '400px', height: '250px' }}>
|
||||
<WidgetRenderer
|
||||
widget={args.widget}
|
||||
displayDragHandle={args.displayDragHandle}
|
||||
onRemove={args.onRemove}
|
||||
onEdit={args.onEdit}
|
||||
/>
|
||||
<WidgetRenderer widget={args.widget} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -298,12 +268,7 @@ export const LargeWidget: Story = {
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '600px', height: '400px' }}>
|
||||
<WidgetRenderer
|
||||
widget={args.widget}
|
||||
displayDragHandle={args.displayDragHandle}
|
||||
onRemove={args.onRemove}
|
||||
onEdit={args.onEdit}
|
||||
/>
|
||||
<WidgetRenderer widget={args.widget} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -341,12 +306,7 @@ export const WideWidget: Story = {
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '800px', height: '200px' }}>
|
||||
<WidgetRenderer
|
||||
widget={args.widget}
|
||||
displayDragHandle={args.displayDragHandle}
|
||||
onRemove={args.onRemove}
|
||||
onEdit={args.onEdit}
|
||||
/>
|
||||
<WidgetRenderer widget={args.widget} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -386,12 +346,7 @@ export const TallWidget: Story = {
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '300px', height: '500px' }}>
|
||||
<WidgetRenderer
|
||||
widget={args.widget}
|
||||
displayDragHandle={args.displayDragHandle}
|
||||
onRemove={args.onRemove}
|
||||
onEdit={args.onEdit}
|
||||
/>
|
||||
<WidgetRenderer widget={args.widget} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
-29
@@ -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 (
|
||||
<PageContainer>
|
||||
<PageHeader title={<Breadcrumb links={links} />}>
|
||||
{actionButton}
|
||||
</PageHeader>
|
||||
<PageBody>{children}</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = () => {
|
||||
<StyledPageContainer
|
||||
animate={{
|
||||
marginLeft:
|
||||
isSettingsPage &&
|
||||
!isMobile &&
|
||||
!useShowFullScreen &&
|
||||
!isPageLayoutEditor
|
||||
isSettingsPage && !isMobile && !useShowFullScreen
|
||||
? (windowsWidth -
|
||||
(OBJECT_SETTINGS_WIDTH +
|
||||
NAV_DRAWER_WIDTHS.menu.desktop.expanded +
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
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 { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '@/page-layout/constants/SettingsPageLayoutTabsInstanceId';
|
||||
import { useChangePageLayoutDragSelection } from '@/page-layout/hooks/useChangePageLayoutDragSelection';
|
||||
import { useCreatePageLayoutTab } from '@/page-layout/hooks/useCreatePageLayoutTab';
|
||||
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { useEndPageLayoutDragSelection } from '@/page-layout/hooks/useEndPageLayoutDragSelection';
|
||||
import { usePageLayoutDraftState } from '@/page-layout/hooks/usePageLayoutDraftState';
|
||||
import { usePageLayoutHandleLayoutChange } from '@/page-layout/hooks/usePageLayoutHandleLayoutChange';
|
||||
import { usePageLayoutSaveHandler } from '@/page-layout/hooks/usePageLayoutSaveHandler';
|
||||
import { useStartPageLayoutDragSelection } from '@/page-layout/hooks/useStartPageLayoutDragSelection';
|
||||
import { WidgetType } from '@/page-layout/mocks/mockWidgets';
|
||||
import { pageLayoutCurrentBreakpointState } from '@/page-layout/states/pageLayoutCurrentBreakpointState';
|
||||
import { pageLayoutCurrentLayoutsState } from '@/page-layout/states/pageLayoutCurrentLayoutsState';
|
||||
import { pageLayoutEditingWidgetIdState } from '@/page-layout/states/pageLayoutEditingWidgetIdState';
|
||||
import { pageLayoutSelectedCellsState } from '@/page-layout/states/pageLayoutSelectedCellsState';
|
||||
import { type PageLayoutWidgetWithData } from '@/page-layout/types/pageLayoutTypes';
|
||||
import { calculateTotalGridRows } from '@/page-layout/utils/calculateTotalGridRows';
|
||||
import { generateCellId } from '@/page-layout/utils/generateCellId';
|
||||
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 { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsPageFullWidthContainer } from '@/settings/components/SettingsPageFullWidthContainer';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useCallback, useMemo, useRef, useState } 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 { useParams } from 'react-router-dom';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { IconAppWindow, IconPlus } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
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<ExtendedResponsiveProps>;
|
||||
|
||||
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<HTMLDivElement>(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 (
|
||||
<>
|
||||
<PageLayoutInitializationEffect layoutId={id} isEditMode={!!isEditMode} />
|
||||
<SettingsPageFullWidthContainer
|
||||
links={[
|
||||
{
|
||||
children: t`Settings`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`Page Layouts`,
|
||||
href: getSettingsPath(SettingsPath.PageLayout),
|
||||
},
|
||||
{
|
||||
children: (
|
||||
<TitleInput
|
||||
instanceId="page-layout-name-input"
|
||||
placeholder={t`Layout Name`}
|
||||
value={pageLayoutDraft.name}
|
||||
onChange={(value) =>
|
||||
setPageLayoutDraft((prev) => ({ ...prev, name: value }))
|
||||
}
|
||||
sizeVariant="md"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
actionButton={
|
||||
<StyledActionButtonContainer>
|
||||
<SaveAndCancelButtons
|
||||
onSave={handleSaveClick}
|
||||
onCancel={handleCancel}
|
||||
isLoading={isSaving}
|
||||
isSaveDisabled={
|
||||
!isDirty ||
|
||||
!pageLayoutDraft.name.trim() ||
|
||||
allWidgets.length === 0
|
||||
}
|
||||
/>
|
||||
{!isEmptyState && (
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`Add Widget`}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={handleOpenAddWidget}
|
||||
/>
|
||||
)}
|
||||
</StyledActionButtonContainer>
|
||||
}
|
||||
>
|
||||
{pageLayoutDraft.tabs.length > 0 && (
|
||||
<StyledTabList
|
||||
tabs={tabListTabs}
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID}
|
||||
onAddTab={handleAddTab}
|
||||
/>
|
||||
)}
|
||||
<StyledGridContainer ref={gridContainerRef}>
|
||||
<StyledGridOverlay
|
||||
isDragSelecting={pageLayoutCurrentBreakpoint !== 'mobile'}
|
||||
breakpoint={pageLayoutCurrentBreakpoint}
|
||||
>
|
||||
{Array.from({
|
||||
length:
|
||||
(pageLayoutCurrentBreakpoint === 'mobile' ? 1 : 12) * gridRows,
|
||||
}).map((_, i) => {
|
||||
const cols = pageLayoutCurrentBreakpoint === 'mobile' ? 1 : 12;
|
||||
const col = i % cols;
|
||||
const row = Math.floor(i / cols);
|
||||
const cellId = generateCellId(col, row);
|
||||
return (
|
||||
<StyledGridCell
|
||||
key={i}
|
||||
data-selectable-id={cellId}
|
||||
isSelected={pageLayoutSelectedCells.has(cellId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledGridOverlay>
|
||||
<ResponsiveGridLayout
|
||||
className="layout"
|
||||
layouts={
|
||||
isEmptyState || !activeTabId
|
||||
? EMPTY_LAYOUT
|
||||
: (pageLayoutCurrentLayouts[activeTabId] ?? EMPTY_LAYOUT)
|
||||
}
|
||||
breakpoints={PAGE_LAYOUT_CONFIG.breakpoints}
|
||||
cols={PAGE_LAYOUT_CONFIG.columns}
|
||||
rowHeight={55}
|
||||
maxCols={12}
|
||||
containerPadding={[0, 0]}
|
||||
margin={[8, 8]}
|
||||
isDraggable={pageLayoutCurrentBreakpoint !== 'mobile'}
|
||||
isResizable={pageLayoutCurrentBreakpoint !== 'mobile'}
|
||||
draggableHandle=".drag-handle"
|
||||
compactType="vertical"
|
||||
preventCollision={false}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
onBreakpointChange={(newBreakpoint) =>
|
||||
setPageLayoutCurrentBreakpoint(
|
||||
newBreakpoint as PageLayoutBreakpoint,
|
||||
)
|
||||
}
|
||||
>
|
||||
{isEmptyState ? (
|
||||
<div key="empty-placeholder" data-select-disable="true">
|
||||
<WidgetPlaceholder onClick={handleOpenAddWidget} />
|
||||
</div>
|
||||
) : (
|
||||
activeTabWidgets.map((widget) => (
|
||||
<div key={widget.id} data-select-disable="true">
|
||||
<WidgetRenderer
|
||||
widget={widget as Widget}
|
||||
displayDragHandle={true}
|
||||
onRemove={() => deletePageLayoutWidget(widget.id)}
|
||||
onEdit={() => handleEditWidget(widget.id)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</ResponsiveGridLayout>
|
||||
{pageLayoutCurrentBreakpoint !== 'mobile' && (
|
||||
<DragSelect
|
||||
selectableItemsContainerRef={gridContainerRef}
|
||||
onDragSelectionStart={startPageLayoutDragSelection}
|
||||
onDragSelectionChange={changePageLayoutDragSelection}
|
||||
onDragSelectionEnd={endPageLayoutDragSelection}
|
||||
/>
|
||||
)}
|
||||
</StyledGridContainer>
|
||||
</SettingsPageFullWidthContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
import { savedPageLayoutsState } from '@/page-layout/states/savedPageLayoutsState';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
H2Title,
|
||||
IconChevronRight,
|
||||
IconLayoutList,
|
||||
IconPlus,
|
||||
IconTrashX,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { UndecoratedLink } from 'twenty-ui/navigation';
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
grid-template-columns: 1fr 180px 80px 80px 36px 36px;
|
||||
`;
|
||||
|
||||
const StyledNameTableCell = styled(TableCell)`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledActionTableCell = styled(TableCell)`
|
||||
justify-content: center;
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledIconChevronRight = styled(IconChevronRight)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
const StyledIconDelete = styled(IconTrashX)`
|
||||
color: ${({ theme }) => theme.font.color.danger};
|
||||
`;
|
||||
|
||||
export const SettingsPageLayouts = () => {
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
const [savedPageLayouts, setSavedPageLayouts] = useRecoilState(
|
||||
savedPageLayoutsState,
|
||||
);
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setSavedPageLayouts((prev) => prev.filter((layout) => layout.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`Page Layouts`}
|
||||
actionButton={
|
||||
<UndecoratedLink to={getSettingsPath(SettingsPath.PageLayoutNew)}>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`Add layout`}
|
||||
accent="blue"
|
||||
size="small"
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: <Trans>Workspace</Trans>,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{ children: <Trans>Page Layouts</Trans> },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title title={t`Existing layouts`} />
|
||||
|
||||
<Table>
|
||||
<StyledTableRow>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>{t`Type`}</TableHeader>
|
||||
<TableHeader>{t`Tabs`}</TableHeader>
|
||||
<TableHeader>{t`Widgets`}</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</StyledTableRow>
|
||||
{savedPageLayouts.map((layout) => (
|
||||
<StyledTableRow
|
||||
key={layout.id}
|
||||
to={getSettingsPath(SettingsPath.PageLayoutEdit, {
|
||||
id: layout.id,
|
||||
})}
|
||||
>
|
||||
<StyledNameTableCell>
|
||||
<IconLayoutList
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
{layout.name}
|
||||
</StyledNameTableCell>
|
||||
<TableCell>{layout.type}</TableCell>
|
||||
<TableCell>{layout.tabs.length}</TableCell>
|
||||
<TableCell>
|
||||
{layout.tabs.reduce(
|
||||
(total, tab) => total + tab.widgets.length,
|
||||
0,
|
||||
)}
|
||||
</TableCell>
|
||||
<StyledActionTableCell
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDelete(layout.id);
|
||||
}}
|
||||
>
|
||||
<StyledIconDelete
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledActionTableCell>
|
||||
<StyledActionTableCell>
|
||||
<StyledIconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledActionTableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
@@ -54,7 +54,4 @@ export enum SettingsPath {
|
||||
RoleDetail = 'roles/:roleId',
|
||||
RoleObjectLevel = 'roles/:roleId/object/:objectMetadataId',
|
||||
RoleAddObjectLevel = 'roles/:roleId/add-object-permission',
|
||||
PageLayout = 'page-layout',
|
||||
PageLayoutNew = 'page-layout/new',
|
||||
PageLayoutEdit = 'page-layout/:id',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user