Create the Dashboard record show page (#14423)
Closes https://github.com/twentyhq/core-team-issues/issues/1438 - Reorganized PageLayout module - Created `DashboardRenderer` and `PageLayoutRenderer` - Created stories for the `PageLayoutRenderer` - Refactored the Widget components https://github.com/user-attachments/assets/27e9ac8f-b237-4c21-8494-3fab6d65af3a
This commit is contained in:
-98
@@ -1,98 +0,0 @@
|
||||
import { GraphWidgetBarChart } from '@/dashboards/widgets/graph/components/GraphWidgetBarChart';
|
||||
import { GraphWidgetGaugeChart } from '@/dashboards/widgets/graph/components/GraphWidgetGaugeChart';
|
||||
import { GraphWidgetLineChart } from '@/dashboards/widgets/graph/components/GraphWidgetLineChart';
|
||||
import { GraphWidgetNumberChart } from '@/dashboards/widgets/graph/components/GraphWidgetNumberChart';
|
||||
import { GraphWidgetPieChart } from '@/dashboards/widgets/graph/components/GraphWidgetPieChart';
|
||||
import { GraphType } from '../mocks/mockWidgets';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
|
||||
type GraphWidgetRendererProps = {
|
||||
widget: PageLayoutWidget;
|
||||
};
|
||||
|
||||
export const GraphWidgetRenderer = ({ widget }: GraphWidgetRendererProps) => {
|
||||
const graphType = widget.configuration?.graphType;
|
||||
|
||||
if (!graphType || typeof graphType !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Object.values(GraphType).includes(graphType as GraphType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (graphType as GraphType) {
|
||||
case GraphType.NUMBER:
|
||||
return (
|
||||
<GraphWidgetNumberChart
|
||||
value={widget.data.value}
|
||||
trendPercentage={widget.data.trendPercentage}
|
||||
/>
|
||||
);
|
||||
|
||||
case GraphType.GAUGE:
|
||||
return (
|
||||
<GraphWidgetGaugeChart
|
||||
data={{
|
||||
value: widget.data.value,
|
||||
min: widget.data.min,
|
||||
max: widget.data.max,
|
||||
label: widget.data.label,
|
||||
}}
|
||||
displayType="percentage"
|
||||
showValue
|
||||
id={`gauge-chart-${widget.id}`}
|
||||
/>
|
||||
);
|
||||
|
||||
case GraphType.PIE:
|
||||
return (
|
||||
<GraphWidgetPieChart
|
||||
data={widget.data.items}
|
||||
showLegend
|
||||
displayType="percentage"
|
||||
id={`pie-chart-${widget.id}`}
|
||||
/>
|
||||
);
|
||||
|
||||
case GraphType.BAR:
|
||||
return (
|
||||
<GraphWidgetBarChart
|
||||
data={widget.data.items}
|
||||
indexBy={widget.data.indexBy}
|
||||
keys={widget.data.keys}
|
||||
seriesLabels={widget.data.seriesLabels}
|
||||
layout={widget.data.layout}
|
||||
showLegend
|
||||
showGrid
|
||||
displayType="number"
|
||||
id={`bar-chart-${widget.id}`}
|
||||
/>
|
||||
);
|
||||
|
||||
case GraphType.LINE:
|
||||
return (
|
||||
<GraphWidgetLineChart
|
||||
id={`line-chart-${widget.id}`}
|
||||
data={widget.data.series}
|
||||
enableArea={widget.data.enableArea}
|
||||
showLegend={widget.data.showLegend}
|
||||
showGrid={widget.data.showGrid}
|
||||
enablePoints={widget.data.enablePoints}
|
||||
xAxisLabel={widget.data.xAxisLabel}
|
||||
yAxisLabel={widget.data.yAxisLabel}
|
||||
displayType={widget.data.displayType}
|
||||
prefix={widget.data.prefix}
|
||||
suffix={widget.data.suffix}
|
||||
xScale={widget.data.xScale}
|
||||
yScale={widget.data.yScale}
|
||||
curve={widget.data.curve}
|
||||
stackedArea={widget.data.stackedArea}
|
||||
enableSlices={widget.data.enableSlices}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
-113
@@ -1,113 +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 { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import {
|
||||
pageLayoutCurrentLayoutsState,
|
||||
type TabLayouts,
|
||||
} from '../states/pageLayoutCurrentLayoutsState';
|
||||
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
import { pageLayoutPersistedState } from '../states/pageLayoutPersistedState';
|
||||
import {
|
||||
PageLayoutType,
|
||||
savedPageLayoutsState,
|
||||
type SavedPageLayout,
|
||||
} from '../states/savedPageLayoutsState';
|
||||
|
||||
type PageLayoutInitializationEffectProps = {
|
||||
layoutId: string | undefined;
|
||||
isEditMode: boolean;
|
||||
};
|
||||
|
||||
export const PageLayoutInitializationEffect = ({
|
||||
layoutId,
|
||||
isEditMode,
|
||||
}: PageLayoutInitializationEffectProps) => {
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const savedPageLayouts = useRecoilValue(savedPageLayoutsState);
|
||||
|
||||
const initializePageLayout = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(layout: SavedPageLayout | 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;
|
||||
initializePageLayout(existingLayout);
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, [
|
||||
layoutId,
|
||||
savedPageLayouts,
|
||||
initializePageLayout,
|
||||
isInitialized,
|
||||
isEditMode,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { type ReactNode } from 'react';
|
||||
import { IconGripVertical, IconPencil, IconX } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import {
|
||||
AnimatedPlaceholder,
|
||||
AnimatedPlaceholderEmptyContainer,
|
||||
AnimatedPlaceholderEmptySubTitle,
|
||||
AnimatedPlaceholderEmptyTextContainer,
|
||||
AnimatedPlaceholderEmptyTitle,
|
||||
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
|
||||
} from 'twenty-ui/layout';
|
||||
|
||||
const StyledPlaceholderContainer = styled.div<{ isEmpty?: boolean }>`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
&:hover {
|
||||
cursor: ${({ isEmpty }) => (isEmpty ? 'pointer' : 'default')};
|
||||
border: 1px solid ${({ theme }) => theme.color.blue};
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledDragHandleButton = styled(IconButton)`
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
flex: 1;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
type PageLayoutWidgetPlaceholderProps = {
|
||||
title?: string;
|
||||
onRemove?: () => void;
|
||||
onEdit?: () => void;
|
||||
children?: ReactNode;
|
||||
isEmpty?: boolean;
|
||||
};
|
||||
|
||||
export const PageLayoutWidgetPlaceholder = ({
|
||||
title = 'Graph Title',
|
||||
onRemove,
|
||||
onEdit,
|
||||
children,
|
||||
isEmpty = false,
|
||||
}: PageLayoutWidgetPlaceholderProps) => {
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<StyledPlaceholderContainer isEmpty>
|
||||
<StyledHeader>
|
||||
<IconButton
|
||||
Icon={IconGripVertical}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
disabled
|
||||
/>
|
||||
<StyledTitle>Add Widget</StyledTitle>
|
||||
</StyledHeader>
|
||||
<AnimatedPlaceholderEmptyContainer
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
|
||||
>
|
||||
<AnimatedPlaceholder type="noWidgets" />
|
||||
<AnimatedPlaceholderEmptyTextContainer>
|
||||
<AnimatedPlaceholderEmptyTitle>
|
||||
No widgets yet
|
||||
</AnimatedPlaceholderEmptyTitle>
|
||||
<AnimatedPlaceholderEmptySubTitle>
|
||||
Click to add your first widget
|
||||
</AnimatedPlaceholderEmptySubTitle>
|
||||
</AnimatedPlaceholderEmptyTextContainer>
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
</StyledPlaceholderContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledPlaceholderContainer>
|
||||
<StyledHeader>
|
||||
<StyledDragHandleButton
|
||||
Icon={IconGripVertical}
|
||||
className="drag-handle"
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
/>
|
||||
<StyledTitle>{title}</StyledTitle>
|
||||
{onEdit && (
|
||||
<IconButton
|
||||
onClick={onEdit}
|
||||
Icon={IconPencil}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
{onRemove && (
|
||||
<IconButton
|
||||
onClick={onRemove}
|
||||
Icon={IconX}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</StyledHeader>
|
||||
<StyledContent>{children}</StyledContent>
|
||||
</StyledPlaceholderContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { IframeWidget } from '@/dashboards/widgets/iframe/components/IframeWidget';
|
||||
import { isString } from '@sniptt/guards';
|
||||
import { WidgetType } from '../mocks/mockWidgets';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
import { GraphWidgetRenderer } from './GraphWidgetRenderer';
|
||||
|
||||
type WidgetRendererProps = {
|
||||
widget: PageLayoutWidget;
|
||||
};
|
||||
|
||||
export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
switch (widget.type) {
|
||||
case WidgetType.GRAPH:
|
||||
return <GraphWidgetRenderer widget={widget} />;
|
||||
|
||||
case WidgetType.IFRAME: {
|
||||
const url = widget.configuration?.url;
|
||||
return (
|
||||
<IframeWidget url={isString(url) ? url : ''} title={widget.title} />
|
||||
);
|
||||
}
|
||||
|
||||
case WidgetType.VIEW:
|
||||
return null;
|
||||
|
||||
case WidgetType.FIELDS:
|
||||
return null;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
-313
@@ -1,313 +0,0 @@
|
||||
import { GraphWidgetGaugeChart } from '@/dashboards/widgets/graph/components/GraphWidgetGaugeChart';
|
||||
import { GraphWidgetNumberChart } from '@/dashboards/widgets/graph/components/GraphWidgetNumberChart';
|
||||
import { GraphWidgetPieChart } from '@/dashboards/widgets/graph/components/GraphWidgetPieChart';
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { PageLayoutWidgetPlaceholder } from '../PageLayoutWidgetPlaceholder';
|
||||
|
||||
const meta: Meta<typeof PageLayoutWidgetPlaceholder> = {
|
||||
title: 'Modules/Settings/PageLayout/PageLayoutWidgetPlaceholder',
|
||||
component: PageLayoutWidgetPlaceholder,
|
||||
decorators: [ComponentDecorator],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
argTypes: {
|
||||
title: {
|
||||
control: 'text',
|
||||
description: 'Widget title',
|
||||
},
|
||||
isEmpty: {
|
||||
control: 'boolean',
|
||||
description: 'Show empty state',
|
||||
},
|
||||
onRemove: {
|
||||
action: 'onRemove',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof PageLayoutWidgetPlaceholder>;
|
||||
|
||||
export const WithNumberChart: Story = {
|
||||
args: {
|
||||
title: 'Sales Pipeline',
|
||||
children: <GraphWidgetNumberChart value="1,234" trendPercentage={12.5} />,
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '300px', height: '100px' }}>
|
||||
<PageLayoutWidgetPlaceholder
|
||||
title={args.title}
|
||||
isEmpty={args.isEmpty}
|
||||
onRemove={args.onRemove}
|
||||
children={args.children}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithGaugeChart: Story = {
|
||||
args: {
|
||||
title: 'Conversion Rate',
|
||||
children: (
|
||||
<GraphWidgetGaugeChart
|
||||
data={{
|
||||
value: 0.5,
|
||||
min: 0,
|
||||
max: 1,
|
||||
label: 'Conversion rate',
|
||||
}}
|
||||
displayType="percentage"
|
||||
showValue
|
||||
id="gauge-chart-story"
|
||||
/>
|
||||
),
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '300px', height: '400px' }}>
|
||||
<PageLayoutWidgetPlaceholder
|
||||
title={args.title}
|
||||
isEmpty={args.isEmpty}
|
||||
onRemove={args.onRemove}
|
||||
children={args.children}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const EmptyState: Story = {
|
||||
args: {
|
||||
isEmpty: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Empty widget placeholder state with dashed border',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '500px', height: '300px' }}>
|
||||
<PageLayoutWidgetPlaceholder
|
||||
title={args.title}
|
||||
isEmpty={args.isEmpty}
|
||||
onRemove={args.onRemove}
|
||||
children={args.children}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithPieChart: Story = {
|
||||
args: {
|
||||
title: 'Lead Distribution',
|
||||
children: (
|
||||
<GraphWidgetPieChart
|
||||
data={[
|
||||
{
|
||||
id: 'qualified',
|
||||
value: 35,
|
||||
label: 'Qualified',
|
||||
to: '/leads/qualified',
|
||||
},
|
||||
{
|
||||
id: 'contacted',
|
||||
value: 25,
|
||||
label: 'Contacted',
|
||||
to: '/leads/contacted',
|
||||
},
|
||||
{
|
||||
id: 'unqualified',
|
||||
value: 20,
|
||||
label: 'Unqualified',
|
||||
to: '/leads/unqualified',
|
||||
},
|
||||
{
|
||||
id: 'proposal',
|
||||
value: 15,
|
||||
label: 'Proposal',
|
||||
to: '/leads/proposal',
|
||||
},
|
||||
{
|
||||
id: 'negotiation',
|
||||
value: 5,
|
||||
label: 'Negotiation',
|
||||
to: '/leads/negotiation',
|
||||
},
|
||||
]}
|
||||
showLegend
|
||||
displayType="percentage"
|
||||
id="pie-chart-story"
|
||||
/>
|
||||
),
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '300px', height: '500px' }}>
|
||||
<PageLayoutWidgetPlaceholder
|
||||
title={args.title}
|
||||
isEmpty={args.isEmpty}
|
||||
onRemove={args.onRemove}
|
||||
children={args.children}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const SmallWidget: Story = {
|
||||
args: {
|
||||
title: 'Small Widget (2x2 grid)',
|
||||
children: <GraphWidgetNumberChart value="42" trendPercentage={5} />,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Simulates a 2x2 grid cell widget',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '300px', height: '100px' }}>
|
||||
<PageLayoutWidgetPlaceholder
|
||||
title={args.title}
|
||||
isEmpty={args.isEmpty}
|
||||
onRemove={args.onRemove}
|
||||
children={args.children}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const MediumWidget: Story = {
|
||||
args: {
|
||||
title: 'Medium Widget (4x3 grid)',
|
||||
children: (
|
||||
<GraphWidgetGaugeChart
|
||||
data={{
|
||||
value: 0.75,
|
||||
min: 0,
|
||||
max: 1,
|
||||
label: 'Progress',
|
||||
}}
|
||||
displayType="percentage"
|
||||
showValue
|
||||
id="gauge-medium"
|
||||
/>
|
||||
),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Simulates a 4x3 grid cell widget',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '400px', height: '250px' }}>
|
||||
<PageLayoutWidgetPlaceholder
|
||||
title={args.title}
|
||||
isEmpty={args.isEmpty}
|
||||
onRemove={args.onRemove}
|
||||
children={args.children}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const LargeWidget: Story = {
|
||||
args: {
|
||||
title: 'Large Widget (6x4 grid)',
|
||||
children: (
|
||||
<GraphWidgetPieChart
|
||||
data={[
|
||||
{ id: 'a', value: 40, label: 'Category A', to: '/a' },
|
||||
{ id: 'b', value: 30, label: 'Category B', to: '/b' },
|
||||
{ id: 'c', value: 20, label: 'Category C', to: '/c' },
|
||||
{ id: 'd', value: 10, label: 'Category D', to: '/d' },
|
||||
]}
|
||||
showLegend
|
||||
displayType="percentage"
|
||||
id="pie-large"
|
||||
/>
|
||||
),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Simulates a 6x4 grid cell widget',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '600px', height: '400px' }}>
|
||||
<PageLayoutWidgetPlaceholder
|
||||
title={args.title}
|
||||
isEmpty={args.isEmpty}
|
||||
onRemove={args.onRemove}
|
||||
children={args.children}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WideWidget: Story = {
|
||||
args: {
|
||||
title: 'Wide Widget (8x2 grid)',
|
||||
children: (
|
||||
<GraphWidgetNumberChart value="1,234,567" trendPercentage={23.4} />
|
||||
),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Simulates a wide 8x2 grid cell widget',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '800px', height: '200px' }}>
|
||||
<PageLayoutWidgetPlaceholder
|
||||
title={args.title}
|
||||
isEmpty={args.isEmpty}
|
||||
onRemove={args.onRemove}
|
||||
children={args.children}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const TallWidget: Story = {
|
||||
args: {
|
||||
title: 'Tall Widget (3x6 grid)',
|
||||
children: (
|
||||
<GraphWidgetGaugeChart
|
||||
data={{
|
||||
value: 0.33,
|
||||
min: 0,
|
||||
max: 1,
|
||||
label: 'Utilization',
|
||||
}}
|
||||
displayType="percentage"
|
||||
showValue
|
||||
id="gauge-tall"
|
||||
/>
|
||||
),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Simulates a tall 3x6 grid cell widget',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (args) => (
|
||||
<div style={{ width: '300px', height: '500px' }}>
|
||||
<PageLayoutWidgetPlaceholder
|
||||
title={args.title}
|
||||
isEmpty={args.isEmpty}
|
||||
onRemove={args.onRemove}
|
||||
children={args.children}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
|
||||
export const EMPTY_LAYOUT: Layouts = {
|
||||
desktop: [{ i: 'empty-placeholder', x: 0, y: 0, w: 4, h: 4, static: true }],
|
||||
mobile: [{ i: 'empty-placeholder', x: 0, y: 0, w: 1, h: 4, static: true }],
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export const GRID_BUFFER_ROWS = 10;
|
||||
@@ -1 +0,0 @@
|
||||
export const GRID_MIN_ROWS = 25;
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||
|
||||
export const PAGE_LAYOUT_CONFIG = {
|
||||
breakpoints: {
|
||||
desktop: MOBILE_VIEWPORT,
|
||||
mobile: 0,
|
||||
},
|
||||
columns: {
|
||||
desktop: 12,
|
||||
mobile: 1,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type PageLayoutBreakpoint = 'desktop' | 'mobile';
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
export const SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID =
|
||||
'settings-page-layout-tabs';
|
||||
-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',
|
||||
);
|
||||
});
|
||||
});
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
import { pageLayoutCurrentLayoutsState } from '@/settings/page-layout/states/pageLayoutCurrentLayoutsState';
|
||||
import { pageLayoutDraftState } from '@/settings/page-layout/states/pageLayoutDraftState';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
import { useCreatePageLayoutTab } from '../useCreatePageLayoutTab';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useCreatePageLayoutTab', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create a new tab with default title', () => {
|
||||
const uuidModule = require('uuid');
|
||||
uuidModule.v4.mockReturnValue('mock-uuid');
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createTab: useCreatePageLayoutTab(),
|
||||
pageLayoutCurrentLayouts: useRecoilValue(pageLayoutCurrentLayoutsState),
|
||||
pageLayoutDraft: useRecoilValue(pageLayoutDraftState),
|
||||
}),
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
let newTabId: string;
|
||||
act(() => {
|
||||
newTabId = result.current.createTab.createPageLayoutTab();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutDraft.tabs[0].id).toBe('tab-mock-uuid');
|
||||
expect(result.current.pageLayoutDraft.tabs[0].title).toBe('Tab 1');
|
||||
expect(result.current.pageLayoutDraft.tabs[0].position).toBe(0);
|
||||
expect(result.current.pageLayoutDraft.tabs[0].widgets).toEqual([]);
|
||||
|
||||
expect(result.current.pageLayoutCurrentLayouts['tab-mock-uuid']).toEqual({
|
||||
desktop: [],
|
||||
mobile: [],
|
||||
});
|
||||
|
||||
expect(newTabId!).toBe('tab-mock-uuid');
|
||||
});
|
||||
|
||||
it('should create a new tab with custom title', () => {
|
||||
const uuidModule = require('uuid');
|
||||
uuidModule.v4.mockReturnValue('mock-uuid');
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createTab: useCreatePageLayoutTab(),
|
||||
pageLayoutDraft: useRecoilValue(pageLayoutDraftState),
|
||||
}),
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.createTab.createPageLayoutTab('Custom Tab Name');
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutDraft.tabs[0].title).toBe(
|
||||
'Custom Tab Name',
|
||||
);
|
||||
});
|
||||
|
||||
it('should increment position for subsequent tabs', () => {
|
||||
const uuidModule = require('uuid');
|
||||
uuidModule.v4
|
||||
.mockReturnValueOnce('mock-uuid')
|
||||
.mockReturnValueOnce('mock-uuid-2');
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createTab: useCreatePageLayoutTab(),
|
||||
pageLayoutDraft: useRecoilValue(pageLayoutDraftState),
|
||||
}),
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.createTab.createPageLayoutTab();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.createTab.createPageLayoutTab();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutDraft.tabs).toHaveLength(2);
|
||||
expect(result.current.pageLayoutDraft.tabs[0].position).toBe(0);
|
||||
expect(result.current.pageLayoutDraft.tabs[0].title).toBe('Tab 1');
|
||||
expect(result.current.pageLayoutDraft.tabs[1].position).toBe(1);
|
||||
expect(result.current.pageLayoutDraft.tabs[1].title).toBe('Tab 2');
|
||||
});
|
||||
|
||||
it('should create isolated layouts for multiple tabs', () => {
|
||||
const uuidModule = require('uuid');
|
||||
uuidModule.v4
|
||||
.mockReturnValueOnce('mock-uuid-1')
|
||||
.mockReturnValueOnce('mock-uuid-2');
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createTab: useCreatePageLayoutTab(),
|
||||
pageLayoutCurrentLayouts: useRecoilValue(pageLayoutCurrentLayoutsState),
|
||||
}),
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
let tabId1: string = '';
|
||||
act(() => {
|
||||
tabId1 = result.current.createTab.createPageLayoutTab();
|
||||
});
|
||||
|
||||
let tabId2: string = '';
|
||||
act(() => {
|
||||
tabId2 = result.current.createTab.createPageLayoutTab();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutCurrentLayouts[tabId1]).toEqual({
|
||||
desktop: [],
|
||||
mobile: [],
|
||||
});
|
||||
expect(result.current.pageLayoutCurrentLayouts[tabId2]).toEqual({
|
||||
desktop: [],
|
||||
mobile: [],
|
||||
});
|
||||
expect(tabId1).not.toBe(tabId2);
|
||||
});
|
||||
});
|
||||
-205
@@ -1,205 +0,0 @@
|
||||
import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '@/settings/page-layout/constants/SettingsPageLayoutTabsInstanceId';
|
||||
import {
|
||||
GraphType,
|
||||
WidgetType,
|
||||
} from '@/settings/page-layout/mocks/mockWidgets';
|
||||
import { pageLayoutCurrentLayoutsState } from '@/settings/page-layout/states/pageLayoutCurrentLayoutsState';
|
||||
import { pageLayoutDraftState } from '@/settings/page-layout/states/pageLayoutDraftState';
|
||||
import { PageLayoutType } from '@/settings/page-layout/states/savedPageLayoutsState';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useCreatePageLayoutWidget } from '../useCreatePageLayoutWidget';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid'),
|
||||
}));
|
||||
|
||||
describe('useCreatePageLayoutWidget', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create widget in the correct tab with isolated layouts', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setActiveTabId = useSetRecoilState(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID,
|
||||
}),
|
||||
);
|
||||
const setPageLayoutDraft = useSetRecoilState(pageLayoutDraftState);
|
||||
const pageLayoutDraft = useRecoilValue(pageLayoutDraftState);
|
||||
const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets);
|
||||
const pageLayoutCurrentLayouts = useRecoilValue(
|
||||
pageLayoutCurrentLayoutsState,
|
||||
);
|
||||
const createWidget = useCreatePageLayoutWidget();
|
||||
return {
|
||||
setActiveTabId,
|
||||
setPageLayoutDraft,
|
||||
allWidgets,
|
||||
pageLayoutCurrentLayouts,
|
||||
createWidget,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
pageLayoutId: '',
|
||||
widgets: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
result.current.setActiveTabId('tab-1');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.createWidget.createPageLayoutWidget(
|
||||
WidgetType.GRAPH,
|
||||
GraphType.BAR,
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.allWidgets).toHaveLength(1);
|
||||
expect(result.current.allWidgets[0].pageLayoutTabId).toBe('tab-1');
|
||||
|
||||
expect(result.current.pageLayoutCurrentLayouts['tab-1']).toBeDefined();
|
||||
expect(
|
||||
result.current.pageLayoutCurrentLayouts['tab-1'].desktop,
|
||||
).toHaveLength(1);
|
||||
expect(result.current.pageLayoutCurrentLayouts['tab-2']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle different graph types', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setPageLayoutDraft = useSetRecoilState(pageLayoutDraftState);
|
||||
const setActiveTabId = useSetRecoilState(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID,
|
||||
}),
|
||||
);
|
||||
const pageLayoutDraft = useRecoilValue(pageLayoutDraftState);
|
||||
const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets);
|
||||
const pageLayoutCurrentLayouts = useRecoilValue(
|
||||
pageLayoutCurrentLayoutsState,
|
||||
);
|
||||
const createWidget = useCreatePageLayoutWidget();
|
||||
return {
|
||||
setPageLayoutDraft,
|
||||
setActiveTabId,
|
||||
pageLayoutDraft,
|
||||
allWidgets,
|
||||
pageLayoutCurrentLayouts,
|
||||
createWidget,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
pageLayoutId: '',
|
||||
widgets: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
result.current.setActiveTabId('tab-1');
|
||||
});
|
||||
|
||||
const graphTypes = [
|
||||
GraphType.NUMBER,
|
||||
GraphType.GAUGE,
|
||||
GraphType.PIE,
|
||||
GraphType.BAR,
|
||||
];
|
||||
|
||||
graphTypes.forEach((graphType) => {
|
||||
act(() => {
|
||||
result.current.createWidget.createPageLayoutWidget(
|
||||
WidgetType.GRAPH,
|
||||
graphType,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.allWidgets).toHaveLength(4);
|
||||
|
||||
graphTypes.forEach((graphType, index) => {
|
||||
const widget = result.current.allWidgets[index];
|
||||
expect(widget.type).toBe(WidgetType.GRAPH);
|
||||
expect(widget.pageLayoutTabId).toBe('tab-1');
|
||||
expect(widget.configuration?.graphType).toBe(graphType);
|
||||
expect(widget.id).toBe('widget-mock-uuid');
|
||||
expect(widget.data).toBeDefined();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutCurrentLayouts['tab-1']).toBeDefined();
|
||||
expect(
|
||||
result.current.pageLayoutCurrentLayouts['tab-1'].desktop,
|
||||
).toHaveLength(4);
|
||||
expect(
|
||||
result.current.pageLayoutCurrentLayouts['tab-1'].mobile,
|
||||
).toHaveLength(4);
|
||||
|
||||
expect(result.current.pageLayoutDraft.tabs[0].widgets).toHaveLength(4);
|
||||
});
|
||||
|
||||
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 createWidget = useCreatePageLayoutWidget();
|
||||
return { allWidgets, pageLayoutCurrentLayouts, createWidget };
|
||||
},
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.createWidget.createPageLayoutWidget(
|
||||
WidgetType.GRAPH,
|
||||
GraphType.BAR,
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.allWidgets).toHaveLength(0);
|
||||
expect(Object.keys(result.current.pageLayoutCurrentLayouts)).toHaveLength(
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { useDeletePageLayoutWidget } from '../useDeletePageLayoutWidget';
|
||||
|
||||
describe('useDeletePageLayoutWidget', () => {
|
||||
it('should remove widget from all states', () => {
|
||||
const { result } = renderHook(() => useDeletePageLayoutWidget(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.deletePageLayoutWidget('widget-1');
|
||||
});
|
||||
|
||||
expect(typeof result.current.deletePageLayoutWidget).toBe('function');
|
||||
});
|
||||
|
||||
it('should handle removing non-existent widget', () => {
|
||||
const { result } = renderHook(() => useDeletePageLayoutWidget(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.deletePageLayoutWidget('non-existent-widget');
|
||||
});
|
||||
|
||||
expect(typeof result.current.deletePageLayoutWidget).toBe('function');
|
||||
});
|
||||
|
||||
it('should handle empty layouts', () => {
|
||||
const { result } = renderHook(() => useDeletePageLayoutWidget(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.deletePageLayoutWidget('any-widget');
|
||||
});
|
||||
|
||||
expect(typeof result.current.deletePageLayoutWidget).toBe('function');
|
||||
});
|
||||
});
|
||||
-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);
|
||||
});
|
||||
});
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
import {
|
||||
GraphType,
|
||||
WidgetType,
|
||||
} from '@/settings/page-layout/mocks/mockWidgets';
|
||||
import { PageLayoutType } from '@/settings/page-layout/states/savedPageLayoutsState';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { usePageLayoutDraftState } from '../usePageLayoutDraftState';
|
||||
|
||||
describe('usePageLayoutDraftState', () => {
|
||||
it('should detect dirty state when draft differs from persisted', () => {
|
||||
const { result } = renderHook(() => usePageLayoutDraftState(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: ' ',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
tabs: [],
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.isDirty).toBe(false);
|
||||
expect(result.current.canSave).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow updating draft state', () => {
|
||||
const { result } = renderHook(() => usePageLayoutDraftState(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: 'Updated Name',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
tabs: [],
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutDraft.name).toBe('Updated Name');
|
||||
expect(result.current.canSave).toBe(true);
|
||||
expect(result.current.isDirty).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect changes in widgets', () => {
|
||||
const { result } = renderHook(() => usePageLayoutDraftState(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
pageLayoutId: '',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
widgets: [
|
||||
{
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'New Widget',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 2, column: 2, rowSpan: 2, columnSpan: 2 },
|
||||
configuration: { graphType: GraphType.BAR },
|
||||
data: {},
|
||||
objectMetadataId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.isDirty).toBe(true);
|
||||
expect(result.current.canSave).toBe(true);
|
||||
});
|
||||
});
|
||||
-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);
|
||||
});
|
||||
});
|
||||
-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);
|
||||
});
|
||||
});
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { pageLayoutSelectedCellsState } from '../states/pageLayoutSelectedCellsState';
|
||||
|
||||
export const useChangePageLayoutDragSelection = () => {
|
||||
const changePageLayoutDragSelection = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(cellId: string, selected: boolean) => {
|
||||
set(pageLayoutSelectedCellsState, (prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (selected) {
|
||||
newSet.add(cellId);
|
||||
} else {
|
||||
newSet.delete(cellId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { changePageLayoutDragSelection };
|
||||
};
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '../constants/SettingsPageLayoutTabsInstanceId';
|
||||
import { WidgetType } from '../mocks/mockWidgets';
|
||||
import { pageLayoutCurrentLayoutsState } from '../states/pageLayoutCurrentLayoutsState';
|
||||
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
import { pageLayoutDraggedAreaState } from '../states/pageLayoutDraggedAreaState';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
import { addWidgetToTab } from '../utils/addWidgetToTab';
|
||||
import { createUpdatedTabLayouts } from '../utils/createUpdatedTabLayouts';
|
||||
import { getDefaultWidgetPosition } from '../utils/getDefaultWidgetPosition';
|
||||
|
||||
export const useCreatePageLayoutIframeWidget = () => {
|
||||
const activeTabId = useRecoilComponentValue(
|
||||
activeTabIdComponentState,
|
||||
SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const createPageLayoutIframeWidget = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(title: string, url: string) => {
|
||||
const allTabLayouts = snapshot
|
||||
.getLoadable(pageLayoutCurrentLayoutsState)
|
||||
.getValue();
|
||||
const pageLayoutDraggedArea = snapshot
|
||||
.getLoadable(pageLayoutDraggedAreaState)
|
||||
.getValue();
|
||||
|
||||
if (!activeTabId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const widgetId = `widget-${uuidv4()}`;
|
||||
const defaultSize = { w: 6, h: 6 };
|
||||
const position = getDefaultWidgetPosition(
|
||||
pageLayoutDraggedArea,
|
||||
defaultSize,
|
||||
);
|
||||
|
||||
const newWidget: PageLayoutWidget = {
|
||||
id: widgetId,
|
||||
pageLayoutTabId: activeTabId,
|
||||
title,
|
||||
type: WidgetType.IFRAME,
|
||||
gridPosition: {
|
||||
row: position.y,
|
||||
column: position.x,
|
||||
rowSpan: position.h,
|
||||
columnSpan: position.w,
|
||||
},
|
||||
configuration: {
|
||||
url,
|
||||
},
|
||||
objectMetadataId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
const newLayout = {
|
||||
i: widgetId,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
w: position.w,
|
||||
h: position.h,
|
||||
};
|
||||
|
||||
const updatedLayouts = createUpdatedTabLayouts(
|
||||
allTabLayouts,
|
||||
activeTabId,
|
||||
newLayout,
|
||||
);
|
||||
set(pageLayoutCurrentLayoutsState, updatedLayouts);
|
||||
|
||||
set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
|
||||
}));
|
||||
|
||||
set(pageLayoutDraggedAreaState, null);
|
||||
},
|
||||
[activeTabId],
|
||||
);
|
||||
|
||||
return { createPageLayoutIframeWidget };
|
||||
};
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { pageLayoutCurrentLayoutsState } from '../states/pageLayoutCurrentLayoutsState';
|
||||
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
import { type PageLayoutTab } from '../states/savedPageLayoutsState';
|
||||
import { createEmptyTabLayout } from '../utils/createEmptyTabLayout';
|
||||
|
||||
export const useCreatePageLayoutTab = () => {
|
||||
const createPageLayoutTab = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(title?: string): string => {
|
||||
const pageLayoutDraft = snapshot
|
||||
.getLoadable(pageLayoutDraftState)
|
||||
.getValue();
|
||||
|
||||
const newTabId = `tab-${uuidv4()}`;
|
||||
const newTab: PageLayoutTab = {
|
||||
id: newTabId,
|
||||
title: title || `Tab ${pageLayoutDraft.tabs.length + 1}`,
|
||||
position: pageLayoutDraft.tabs.length,
|
||||
pageLayoutId: '',
|
||||
widgets: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
const updatedTabs = [...pageLayoutDraft.tabs, newTab];
|
||||
|
||||
set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: updatedTabs,
|
||||
}));
|
||||
|
||||
set(pageLayoutCurrentLayoutsState, (prev) =>
|
||||
createEmptyTabLayout(prev, newTabId),
|
||||
);
|
||||
|
||||
return newTabId;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { createPageLayoutTab };
|
||||
};
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '../constants/SettingsPageLayoutTabsInstanceId';
|
||||
import { type GraphType, type WidgetType } from '../mocks/mockWidgets';
|
||||
import { pageLayoutCurrentLayoutsState } from '../states/pageLayoutCurrentLayoutsState';
|
||||
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
import { pageLayoutDraggedAreaState } from '../states/pageLayoutDraggedAreaState';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
import { addWidgetToTab } from '../utils/addWidgetToTab';
|
||||
import { createUpdatedTabLayouts } from '../utils/createUpdatedTabLayouts';
|
||||
import {
|
||||
getDefaultWidgetData,
|
||||
getWidgetSize,
|
||||
getWidgetTitle,
|
||||
} from '../utils/getDefaultWidgetData';
|
||||
import { getDefaultWidgetPosition } from '../utils/getDefaultWidgetPosition';
|
||||
|
||||
export const useCreatePageLayoutWidget = () => {
|
||||
const activeTabId = useRecoilComponentValue(
|
||||
activeTabIdComponentState,
|
||||
SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const createPageLayoutWidget = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(widgetType: WidgetType, graphType: GraphType) => {
|
||||
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) =>
|
||||
w.type === widgetType && w.configuration?.graphType === graphType,
|
||||
).length;
|
||||
const title = getWidgetTitle(graphType, existingWidgetCount);
|
||||
const widgetId = `widget-${uuidv4()}`;
|
||||
|
||||
const defaultSize = getWidgetSize(graphType);
|
||||
const position = getDefaultWidgetPosition(
|
||||
pageLayoutDraggedArea,
|
||||
defaultSize,
|
||||
);
|
||||
|
||||
const newWidget: PageLayoutWidget = {
|
||||
id: widgetId,
|
||||
pageLayoutTabId: activeTabId,
|
||||
title,
|
||||
type: widgetType,
|
||||
gridPosition: {
|
||||
row: position.y,
|
||||
column: position.x,
|
||||
rowSpan: position.h,
|
||||
columnSpan: position.w,
|
||||
},
|
||||
configuration: {
|
||||
graphType,
|
||||
},
|
||||
data: widgetData,
|
||||
objectMetadataId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
const newLayout = {
|
||||
i: widgetId,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
w: position.w,
|
||||
h: position.h,
|
||||
};
|
||||
|
||||
const updatedLayouts = createUpdatedTabLayouts(
|
||||
allTabLayouts,
|
||||
activeTabId,
|
||||
newLayout,
|
||||
);
|
||||
set(pageLayoutCurrentLayoutsState, updatedLayouts);
|
||||
|
||||
set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
|
||||
}));
|
||||
|
||||
set(pageLayoutDraggedAreaState, null);
|
||||
},
|
||||
[activeTabId],
|
||||
);
|
||||
|
||||
return { createPageLayoutWidget };
|
||||
};
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { pageLayoutCurrentLayoutsState } from '../states/pageLayoutCurrentLayoutsState';
|
||||
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
import { removeWidgetFromTab } from '../utils/removeWidgetFromTab';
|
||||
import { removeWidgetLayoutFromTab } from '../utils/removeWidgetLayoutFromTab';
|
||||
|
||||
export const useDeletePageLayoutWidget = () => {
|
||||
const deletePageLayoutWidget = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(widgetId: string) => {
|
||||
const pageLayoutDraft = snapshot
|
||||
.getLoadable(pageLayoutDraftState)
|
||||
.getValue();
|
||||
const allTabLayouts = snapshot
|
||||
.getLoadable(pageLayoutCurrentLayoutsState)
|
||||
.getValue();
|
||||
|
||||
const tabWithWidget = pageLayoutDraft.tabs.find((tab) =>
|
||||
tab.widgets.some((w) => w.id === widgetId),
|
||||
);
|
||||
const tabId = tabWithWidget?.id;
|
||||
|
||||
if (isDefined(tabId)) {
|
||||
const updatedLayouts = removeWidgetLayoutFromTab(
|
||||
allTabLayouts,
|
||||
tabId,
|
||||
widgetId,
|
||||
);
|
||||
set(pageLayoutCurrentLayoutsState, updatedLayouts);
|
||||
|
||||
set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: removeWidgetFromTab(prev.tabs, tabId, widgetId),
|
||||
}));
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { deletePageLayoutWidget };
|
||||
};
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
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 { calculateGridBoundsFromSelectedCells } from '../utils/calculateGridBoundsFromSelectedCells';
|
||||
|
||||
export const useEndPageLayoutDragSelection = () => {
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
|
||||
const endPageLayoutDragSelection = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
const pageLayoutSelectedCells = snapshot
|
||||
.getLoadable(pageLayoutSelectedCellsState)
|
||||
.getValue();
|
||||
|
||||
if (pageLayoutSelectedCells.size > 0) {
|
||||
const draggedBounds = calculateGridBoundsFromSelectedCells(
|
||||
Array.from(pageLayoutSelectedCells),
|
||||
);
|
||||
|
||||
if (isDefined(draggedBounds)) {
|
||||
set(pageLayoutDraggedAreaState, draggedBounds);
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.PageLayoutWidgetTypeSelect,
|
||||
pageTitle: 'Add Widget',
|
||||
pageIcon: IconAppWindow,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
|
||||
set(pageLayoutSelectedCellsState, new Set());
|
||||
}
|
||||
}
|
||||
},
|
||||
[navigateCommandMenu],
|
||||
);
|
||||
|
||||
return { endPageLayoutDragSelection };
|
||||
};
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
import { pageLayoutPersistedState } from '../states/pageLayoutPersistedState';
|
||||
|
||||
export const usePageLayoutDraftState = () => {
|
||||
const [pageLayoutDraft, setPageLayoutDraft] =
|
||||
useRecoilState(pageLayoutDraftState);
|
||||
const pageLayoutPersisted = useRecoilValue(pageLayoutPersistedState);
|
||||
|
||||
const isDirty = pageLayoutPersisted
|
||||
? !isDeeplyEqual(pageLayoutDraft, {
|
||||
name: pageLayoutPersisted.name,
|
||||
type: pageLayoutPersisted.type,
|
||||
objectMetadataId: pageLayoutPersisted.objectMetadataId,
|
||||
tabs: pageLayoutPersisted.tabs,
|
||||
})
|
||||
: pageLayoutDraft.name.trim().length > 0 || pageLayoutDraft.tabs.length > 0;
|
||||
|
||||
const canSave = pageLayoutDraft.name?.trim().length > 0;
|
||||
|
||||
return {
|
||||
pageLayoutDraft,
|
||||
setPageLayoutDraft,
|
||||
isDirty,
|
||||
canSave,
|
||||
};
|
||||
};
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
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 PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
import { convertLayoutsToWidgets } from '../utils/convertLayoutsToWidgets';
|
||||
|
||||
export const usePageLayoutHandleLayoutChange = (activeTabId: string | null) => {
|
||||
const handleLayoutChange = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(_: Layout[], allLayouts: Layouts) => {
|
||||
if (!isDefined(activeTabId)) return;
|
||||
const currentTabLayouts = snapshot
|
||||
.getLoadable(pageLayoutCurrentLayoutsState)
|
||||
.getValue();
|
||||
|
||||
set(pageLayoutCurrentLayoutsState, {
|
||||
...currentTabLayouts,
|
||||
[activeTabId]: allLayouts,
|
||||
});
|
||||
|
||||
const pageLayoutDraft = snapshot
|
||||
.getLoadable(pageLayoutDraftState)
|
||||
.getValue();
|
||||
|
||||
const currentTab = pageLayoutDraft.tabs.find(
|
||||
(tab) => tab.id === activeTabId,
|
||||
);
|
||||
if (!currentTab) return;
|
||||
const updatedWidgets = convertLayoutsToWidgets(
|
||||
currentTab.widgets,
|
||||
allLayouts,
|
||||
);
|
||||
|
||||
if (isDefined(activeTabId)) {
|
||||
set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: prev.tabs.map((tab) => {
|
||||
if (tab.id === activeTabId) {
|
||||
const tabWidgets: PageLayoutWidget[] = updatedWidgets
|
||||
.filter((w) => w.pageLayoutTabId === activeTabId)
|
||||
.map((widget) => ({
|
||||
id: widget.id,
|
||||
pageLayoutTabId: widget.pageLayoutTabId || activeTabId,
|
||||
title: widget.title,
|
||||
type: widget.type,
|
||||
objectMetadataId: null,
|
||||
gridPosition: widget.gridPosition,
|
||||
configuration: widget.configuration || undefined,
|
||||
data: widget.data,
|
||||
createdAt:
|
||||
tab.widgets.find((w) => w.id === widget.id)?.createdAt ||
|
||||
new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
}));
|
||||
return {
|
||||
...tab,
|
||||
widgets: tabWidgets,
|
||||
};
|
||||
}
|
||||
return tab;
|
||||
}),
|
||||
}));
|
||||
}
|
||||
},
|
||||
[activeTabId],
|
||||
);
|
||||
|
||||
return { handleLayoutChange };
|
||||
};
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
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';
|
||||
import {
|
||||
savedPageLayoutsState,
|
||||
type PageLayoutWidget,
|
||||
type SavedPageLayout,
|
||||
} from '../states/savedPageLayoutsState';
|
||||
|
||||
export const usePageLayoutSaveHandler = () => {
|
||||
const navigateSettings = useNavigateSettings();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const isEditMode = id && id !== 'new';
|
||||
|
||||
const savePageLayout = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (widgetsWithPositions?: PageLayoutWidget[]) => {
|
||||
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: SavedPageLayout = {
|
||||
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 };
|
||||
};
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { pageLayoutSelectedCellsState } from '../states/pageLayoutSelectedCellsState';
|
||||
|
||||
export const useStartPageLayoutDragSelection = () => {
|
||||
const startPageLayoutDragSelection = useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
set(pageLayoutSelectedCellsState, new Set());
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { startPageLayoutDragSelection };
|
||||
};
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
|
||||
export const useUpdatePageLayoutWidget = () => {
|
||||
const updatePageLayoutWidget = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(widgetId: string, updates: Partial<PageLayoutWidget>) => {
|
||||
set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: prev.tabs.map((tab) => ({
|
||||
...tab,
|
||||
widgets: tab.widgets.map((widget) =>
|
||||
widget.id === widgetId ? { ...widget, ...updates } : widget,
|
||||
),
|
||||
})),
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { updatePageLayoutWidget };
|
||||
};
|
||||
@@ -1,249 +0,0 @@
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
|
||||
export enum WidgetType {
|
||||
VIEW = 'VIEW',
|
||||
IFRAME = 'IFRAME',
|
||||
FIELDS = 'FIELDS',
|
||||
GRAPH = 'GRAPH',
|
||||
}
|
||||
|
||||
export enum GraphType {
|
||||
NUMBER = 'NUMBER',
|
||||
GAUGE = 'GAUGE',
|
||||
PIE = 'PIE',
|
||||
BAR = 'BAR',
|
||||
LINE = 'LINE',
|
||||
}
|
||||
|
||||
export const mockPageLayoutWidgets: PageLayoutWidget[] = [
|
||||
{
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-overview',
|
||||
type: WidgetType.GRAPH,
|
||||
title: 'Sales Pipeline',
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 2,
|
||||
columnSpan: 3,
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphType.NUMBER,
|
||||
},
|
||||
data: {
|
||||
value: '1,234',
|
||||
trendPercentage: 12.5,
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'widget-2',
|
||||
pageLayoutTabId: 'tab-overview',
|
||||
type: WidgetType.GRAPH,
|
||||
title: 'Conversion Rate',
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 6,
|
||||
rowSpan: 5,
|
||||
columnSpan: 3,
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphType.GAUGE,
|
||||
},
|
||||
data: {
|
||||
value: 0.5,
|
||||
min: 0,
|
||||
max: 1,
|
||||
label: 'Conversion rate',
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'widget-3',
|
||||
pageLayoutTabId: 'tab-analytics',
|
||||
type: WidgetType.GRAPH,
|
||||
title: 'Lead Distribution',
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 2,
|
||||
column: 0,
|
||||
rowSpan: 5,
|
||||
columnSpan: 6,
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphType.PIE,
|
||||
},
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: 'qualified',
|
||||
value: 35,
|
||||
label: 'Qualified',
|
||||
to: '/leads/qualified',
|
||||
},
|
||||
{
|
||||
id: 'contacted',
|
||||
value: 25,
|
||||
label: 'Contacted',
|
||||
to: '/leads/contacted',
|
||||
},
|
||||
{
|
||||
id: 'unqualified',
|
||||
value: 20,
|
||||
label: 'Unqualified',
|
||||
to: '/leads/unqualified',
|
||||
},
|
||||
{
|
||||
id: 'proposal',
|
||||
value: 15,
|
||||
label: 'Proposal',
|
||||
to: '/leads/proposal',
|
||||
},
|
||||
{
|
||||
id: 'negotiation',
|
||||
value: 5,
|
||||
label: 'Negotiation',
|
||||
to: '/leads/negotiation',
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'widget-4',
|
||||
pageLayoutTabId: 'tab-reports',
|
||||
type: WidgetType.GRAPH,
|
||||
title: 'Monthly Performance',
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 9,
|
||||
rowSpan: 8,
|
||||
columnSpan: 4,
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphType.BAR,
|
||||
},
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
month: 'Jan',
|
||||
sales: 120,
|
||||
leads: 45,
|
||||
conversions: 12,
|
||||
to: '/metrics/january',
|
||||
},
|
||||
{
|
||||
month: 'Feb',
|
||||
sales: 150,
|
||||
leads: 52,
|
||||
conversions: 15,
|
||||
to: '/metrics/february',
|
||||
},
|
||||
{
|
||||
month: 'Mar',
|
||||
sales: 180,
|
||||
leads: 48,
|
||||
conversions: 18,
|
||||
to: '/metrics/march',
|
||||
},
|
||||
{
|
||||
month: 'Apr',
|
||||
sales: 140,
|
||||
leads: 60,
|
||||
conversions: 14,
|
||||
to: '/metrics/april',
|
||||
},
|
||||
{
|
||||
month: 'May',
|
||||
sales: 200,
|
||||
leads: 55,
|
||||
conversions: 20,
|
||||
to: '/metrics/may',
|
||||
},
|
||||
],
|
||||
indexBy: 'month',
|
||||
keys: ['sales', 'leads', 'conversions'],
|
||||
showLegend: true,
|
||||
showGrid: true,
|
||||
displayType: 'number',
|
||||
xAxisLabel: 'Month',
|
||||
yAxisLabel: 'Count',
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockLayouts: Layouts = {
|
||||
desktop: [
|
||||
{
|
||||
i: 'widget-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 3,
|
||||
h: 2,
|
||||
},
|
||||
{
|
||||
i: 'widget-2',
|
||||
x: 6,
|
||||
y: 0,
|
||||
w: 3,
|
||||
h: 5,
|
||||
},
|
||||
{
|
||||
i: 'widget-3',
|
||||
x: 0,
|
||||
y: 2,
|
||||
w: 6,
|
||||
h: 5,
|
||||
},
|
||||
{
|
||||
i: 'widget-4',
|
||||
x: 9,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 8,
|
||||
},
|
||||
],
|
||||
mobile: [
|
||||
{
|
||||
i: 'widget-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 1,
|
||||
h: 2,
|
||||
},
|
||||
{
|
||||
i: 'widget-2',
|
||||
x: 0,
|
||||
y: 2,
|
||||
w: 1,
|
||||
h: 5,
|
||||
},
|
||||
{
|
||||
i: 'widget-3',
|
||||
x: 0,
|
||||
y: 7,
|
||||
w: 1,
|
||||
h: 5,
|
||||
},
|
||||
{
|
||||
i: 'widget-4',
|
||||
x: 0,
|
||||
y: 12,
|
||||
w: 1,
|
||||
h: 5,
|
||||
},
|
||||
],
|
||||
};
|
||||
-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',
|
||||
});
|
||||
-9
@@ -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: {},
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
import { PageLayoutType, type SavedPageLayout } from './savedPageLayoutsState';
|
||||
|
||||
export type DraftPageLayout = Omit<
|
||||
SavedPageLayout,
|
||||
'id' | 'createdAt' | 'updatedAt' | 'deletedAt'
|
||||
>;
|
||||
|
||||
export const pageLayoutDraftState = createState<DraftPageLayout>({
|
||||
key: 'pageLayoutDraftState',
|
||||
defaultValue: {
|
||||
name: '',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
tabs: [],
|
||||
},
|
||||
});
|
||||
-13
@@ -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,
|
||||
});
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const pageLayoutEditingWidgetIdState = createState<string | null>({
|
||||
key: 'pageLayoutEditingWidgetIdState',
|
||||
defaultValue: null,
|
||||
});
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
import { type SavedPageLayout } from './savedPageLayoutsState';
|
||||
|
||||
export const pageLayoutPersistedState = createState<
|
||||
SavedPageLayout | undefined
|
||||
>({
|
||||
key: 'pageLayoutPersistedState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const pageLayoutSelectedCellsState = createState<Set<string>>({
|
||||
key: 'pageLayoutSelectedCellsState',
|
||||
defaultValue: new Set(),
|
||||
});
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
import { type WidgetType } from '@/settings/page-layout/mocks/mockWidgets';
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export enum PageLayoutType {
|
||||
DASHBOARD = 'DASHBOARD',
|
||||
RECORD_INDEX = 'RECORD_INDEX',
|
||||
RECORD_PAGE = 'RECORD_PAGE',
|
||||
}
|
||||
|
||||
export type GridPosition = {
|
||||
row: number;
|
||||
column: number;
|
||||
rowSpan: number;
|
||||
columnSpan: number;
|
||||
};
|
||||
|
||||
export type PageLayoutWidget = {
|
||||
id: string;
|
||||
pageLayoutTabId: string;
|
||||
title: string;
|
||||
type: WidgetType;
|
||||
objectMetadataId?: string | null;
|
||||
gridPosition: GridPosition;
|
||||
configuration?: Record<string, unknown> | null;
|
||||
data?: any;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string | null;
|
||||
};
|
||||
|
||||
export type PageLayoutTab = {
|
||||
id: string;
|
||||
title: string;
|
||||
position: number;
|
||||
pageLayoutId: string;
|
||||
widgets: PageLayoutWidget[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string | null;
|
||||
};
|
||||
|
||||
export type SavedPageLayout = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: PageLayoutType;
|
||||
objectMetadataId?: string | null;
|
||||
tabs: PageLayoutTab[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string | null;
|
||||
};
|
||||
|
||||
export const savedPageLayoutsState = createState<SavedPageLayout[]>({
|
||||
key: 'savedPageLayoutsState',
|
||||
defaultValue: [],
|
||||
});
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
import { WidgetType } from '../../mocks/mockWidgets';
|
||||
import {
|
||||
type PageLayoutTab,
|
||||
type PageLayoutWidget,
|
||||
} from '../../states/savedPageLayoutsState';
|
||||
import { addWidgetToTab } from '../addWidgetToTab';
|
||||
|
||||
describe('addWidgetToTab', () => {
|
||||
const mockWidget: PageLayoutWidget = {
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Test Widget',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
|
||||
objectMetadataId: null,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
const mockTabs: PageLayoutTab[] = [
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
pageLayoutId: 'layout-1',
|
||||
widgets: [],
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'tab-2',
|
||||
title: 'Tab 2',
|
||||
position: 1,
|
||||
pageLayoutId: 'layout-1',
|
||||
widgets: [],
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
it('should add widget to the correct tab', () => {
|
||||
const result = addWidgetToTab(mockTabs, 'tab-1', mockWidget);
|
||||
|
||||
expect(result[0].widgets).toHaveLength(1);
|
||||
expect(result[0].widgets[0]).toEqual(mockWidget);
|
||||
expect(result[1].widgets).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not modify other tabs', () => {
|
||||
const result = addWidgetToTab(mockTabs, 'tab-1', mockWidget);
|
||||
|
||||
expect(result[1]).toEqual(mockTabs[1]);
|
||||
expect(result[1].widgets).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle non-existent tab ID gracefully', () => {
|
||||
const result = addWidgetToTab(mockTabs, 'non-existent-tab', mockWidget);
|
||||
|
||||
// All tabs should remain unchanged
|
||||
expect(result[0].widgets).toHaveLength(0);
|
||||
expect(result[1].widgets).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should add multiple widgets to the same tab', () => {
|
||||
const secondWidget: PageLayoutWidget = {
|
||||
...mockWidget,
|
||||
id: 'widget-2',
|
||||
title: 'Second Widget',
|
||||
};
|
||||
|
||||
let result = addWidgetToTab(mockTabs, 'tab-1', mockWidget);
|
||||
result = addWidgetToTab(result, 'tab-1', secondWidget);
|
||||
|
||||
expect(result[0].widgets).toHaveLength(2);
|
||||
expect(result[0].widgets[0]).toEqual(mockWidget);
|
||||
expect(result[0].widgets[1]).toEqual(secondWidget);
|
||||
});
|
||||
|
||||
it('should return a new array without mutating the original', () => {
|
||||
const result = addWidgetToTab(mockTabs, 'tab-1', mockWidget);
|
||||
|
||||
expect(result).not.toBe(mockTabs);
|
||||
expect(mockTabs[0].widgets).toHaveLength(0);
|
||||
expect(result[0].widgets).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
import { calculateGridBoundsFromSelectedCells } from '../calculateGridBoundsFromSelectedCells';
|
||||
|
||||
describe('calculateGridBoundsFromSelectedCells', () => {
|
||||
it('should return null for empty array', () => {
|
||||
expect(calculateGridBoundsFromSelectedCells([])).toBeNull();
|
||||
});
|
||||
|
||||
it('should calculate bounds for single cell', () => {
|
||||
expect(calculateGridBoundsFromSelectedCells(['cell-2-3'])).toEqual({
|
||||
x: 2,
|
||||
y: 3,
|
||||
w: 1,
|
||||
h: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should calculate bounds for rectangular selection', () => {
|
||||
expect(
|
||||
calculateGridBoundsFromSelectedCells([
|
||||
'cell-1-1',
|
||||
'cell-2-1',
|
||||
'cell-1-2',
|
||||
'cell-2-2',
|
||||
]),
|
||||
).toEqual({
|
||||
x: 1,
|
||||
y: 1,
|
||||
w: 2,
|
||||
h: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle non-contiguous selection', () => {
|
||||
expect(
|
||||
calculateGridBoundsFromSelectedCells([
|
||||
'cell-0-0',
|
||||
'cell-5-3',
|
||||
'cell-2-1',
|
||||
]),
|
||||
).toEqual({
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle large grid selections', () => {
|
||||
expect(
|
||||
calculateGridBoundsFromSelectedCells(['cell-0-0', 'cell-11-24']),
|
||||
).toEqual({
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 12,
|
||||
h: 25,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle single row selection', () => {
|
||||
expect(
|
||||
calculateGridBoundsFromSelectedCells([
|
||||
'cell-0-5',
|
||||
'cell-1-5',
|
||||
'cell-2-5',
|
||||
'cell-3-5',
|
||||
]),
|
||||
).toEqual({
|
||||
x: 0,
|
||||
y: 5,
|
||||
w: 4,
|
||||
h: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle single column selection', () => {
|
||||
expect(
|
||||
calculateGridBoundsFromSelectedCells([
|
||||
'cell-3-0',
|
||||
'cell-3-1',
|
||||
'cell-3-2',
|
||||
'cell-3-3',
|
||||
]),
|
||||
).toEqual({
|
||||
x: 3,
|
||||
y: 0,
|
||||
w: 1,
|
||||
h: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle duplicate cell IDs in selection', () => {
|
||||
expect(
|
||||
calculateGridBoundsFromSelectedCells([
|
||||
'cell-1-1',
|
||||
'cell-1-1',
|
||||
'cell-2-2',
|
||||
'cell-2-2',
|
||||
]),
|
||||
).toEqual({
|
||||
x: 1,
|
||||
y: 1,
|
||||
w: 2,
|
||||
h: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle L-shaped selection', () => {
|
||||
expect(
|
||||
calculateGridBoundsFromSelectedCells([
|
||||
'cell-0-0',
|
||||
'cell-1-0',
|
||||
'cell-0-1',
|
||||
'cell-0-2',
|
||||
]),
|
||||
).toEqual({
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 2,
|
||||
h: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle sparse diagonal selection', () => {
|
||||
expect(
|
||||
calculateGridBoundsFromSelectedCells([
|
||||
'cell-0-0',
|
||||
'cell-1-1',
|
||||
'cell-2-2',
|
||||
'cell-3-3',
|
||||
]),
|
||||
).toEqual({
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
import { GRID_MIN_ROWS } from '../../constants/GridMinRows';
|
||||
import { calculateTotalGridRows } from '../calculateTotalGridRows';
|
||||
|
||||
describe('calculateTotalGridRows', () => {
|
||||
it('should return minimum rows for empty layouts', () => {
|
||||
expect(calculateTotalGridRows({})).toBe(GRID_MIN_ROWS);
|
||||
});
|
||||
|
||||
it('should calculate rows based on content when exceeding minimum', () => {
|
||||
const layouts = {
|
||||
desktop: [
|
||||
{ i: '1', x: 0, y: 0, w: 2, h: 2 },
|
||||
{ i: '2', x: 2, y: 20, w: 2, h: 3 },
|
||||
],
|
||||
};
|
||||
expect(calculateTotalGridRows(layouts)).toBe(33);
|
||||
});
|
||||
|
||||
it('should respect minimum rows even with content', () => {
|
||||
const layouts = {
|
||||
desktop: [{ i: '1', x: 0, y: 0, w: 1, h: 1 }],
|
||||
};
|
||||
expect(calculateTotalGridRows(layouts)).toBe(GRID_MIN_ROWS);
|
||||
});
|
||||
|
||||
it('should handle custom min and buffer values', () => {
|
||||
const layouts = {
|
||||
desktop: [{ i: '1', x: 0, y: 10, w: 1, h: 5 }],
|
||||
};
|
||||
expect(calculateTotalGridRows(layouts, 10, 5)).toBe(20);
|
||||
});
|
||||
|
||||
it('should consider both desktop and mobile layouts', () => {
|
||||
const layouts = {
|
||||
desktop: [{ i: '1', x: 0, y: 5, w: 2, h: 2 }],
|
||||
mobile: [{ i: '1', x: 0, y: 25, w: 1, h: 3 }],
|
||||
};
|
||||
expect(calculateTotalGridRows(layouts)).toBe(38);
|
||||
});
|
||||
});
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
import { GraphType, WidgetType } from '../../mocks/mockWidgets';
|
||||
import { type PageLayoutWidget } from '../../states/savedPageLayoutsState';
|
||||
import { convertLayoutsToWidgets } from '../convertLayoutsToWidgets';
|
||||
|
||||
describe('convertLayoutsToWidgets', () => {
|
||||
const mockWidgets: PageLayoutWidget[] = [
|
||||
{
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Widget 1',
|
||||
type: WidgetType.GRAPH,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 2,
|
||||
columnSpan: 2,
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphType.NUMBER,
|
||||
},
|
||||
data: { value: 100 },
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'widget-2',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Widget 2',
|
||||
type: WidgetType.GRAPH,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 2,
|
||||
rowSpan: 2,
|
||||
columnSpan: 2,
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphType.PIE,
|
||||
},
|
||||
data: { items: [] },
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
it('should map layout positions to widgets', () => {
|
||||
const layouts = {
|
||||
desktop: [
|
||||
{ i: 'widget-1', x: 2, y: 3, w: 4, h: 5 },
|
||||
{ i: 'widget-2', x: 6, y: 7, w: 8, h: 9 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = convertLayoutsToWidgets(mockWidgets, layouts);
|
||||
|
||||
expect(result[0].gridPosition).toEqual({
|
||||
column: 2,
|
||||
row: 3,
|
||||
columnSpan: 4,
|
||||
rowSpan: 5,
|
||||
});
|
||||
expect(result[1].gridPosition).toEqual({
|
||||
column: 6,
|
||||
row: 7,
|
||||
columnSpan: 8,
|
||||
rowSpan: 9,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use defaults when layout not found', () => {
|
||||
const layouts = {
|
||||
desktop: [{ i: 'widget-1', x: 1, y: 1, w: 1, h: 1 }],
|
||||
};
|
||||
|
||||
const result = convertLayoutsToWidgets(mockWidgets, layouts);
|
||||
|
||||
expect(result[1].gridPosition).toEqual({
|
||||
column: 0,
|
||||
row: 0,
|
||||
columnSpan: 2,
|
||||
rowSpan: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mobile layout', () => {
|
||||
const layouts = {
|
||||
mobile: [{ i: 'widget-1', x: 0, y: 4, w: 1, h: 6 }],
|
||||
};
|
||||
|
||||
const result = convertLayoutsToWidgets(mockWidgets, layouts);
|
||||
|
||||
expect(result[0].gridPosition).toEqual({
|
||||
column: 0,
|
||||
row: 4,
|
||||
columnSpan: 1,
|
||||
rowSpan: 6,
|
||||
});
|
||||
});
|
||||
});
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
import { type TabLayouts } from '../../states/pageLayoutCurrentLayoutsState';
|
||||
import { createEmptyTabLayout } from '../createEmptyTabLayout';
|
||||
|
||||
describe('createEmptyTabLayout', () => {
|
||||
const mockTabLayouts: TabLayouts = {
|
||||
'tab-1': {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2 }],
|
||||
mobile: [{ i: 'widget-1', x: 0, y: 0, w: 1, h: 2 }],
|
||||
},
|
||||
'tab-2': {
|
||||
desktop: [{ i: 'widget-2', x: 0, y: 0, w: 3, h: 3 }],
|
||||
mobile: [{ i: 'widget-2', x: 0, y: 0, w: 1, h: 3 }],
|
||||
},
|
||||
};
|
||||
|
||||
it('should create empty layout for a new tab', () => {
|
||||
const result = createEmptyTabLayout(mockTabLayouts, 'tab-3');
|
||||
|
||||
expect(result['tab-3']).toBeDefined();
|
||||
expect(result['tab-3'].desktop).toEqual([]);
|
||||
expect(result['tab-3'].mobile).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve existing tabs', () => {
|
||||
const result = createEmptyTabLayout(mockTabLayouts, 'tab-3');
|
||||
|
||||
expect(result['tab-1']).toEqual(mockTabLayouts['tab-1']);
|
||||
expect(result['tab-2']).toEqual(mockTabLayouts['tab-2']);
|
||||
expect(Object.keys(result)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should overwrite existing tab with empty layout', () => {
|
||||
const result = createEmptyTabLayout(mockTabLayouts, 'tab-1');
|
||||
|
||||
expect(result['tab-1'].desktop).toEqual([]);
|
||||
expect(result['tab-1'].mobile).toEqual([]);
|
||||
expect(result['tab-2']).toEqual(mockTabLayouts['tab-2']);
|
||||
});
|
||||
|
||||
it('should work with empty initial state', () => {
|
||||
const emptyLayouts: TabLayouts = {};
|
||||
const result = createEmptyTabLayout(emptyLayouts, 'tab-1');
|
||||
|
||||
expect(result['tab-1']).toBeDefined();
|
||||
expect(result['tab-1'].desktop).toEqual([]);
|
||||
expect(result['tab-1'].mobile).toEqual([]);
|
||||
expect(Object.keys(result)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should return a new object without mutating the original', () => {
|
||||
const originalLayouts = JSON.parse(JSON.stringify(mockTabLayouts));
|
||||
const result = createEmptyTabLayout(mockTabLayouts, 'tab-3');
|
||||
|
||||
expect(result).not.toBe(mockTabLayouts);
|
||||
expect(mockTabLayouts).toEqual(originalLayouts);
|
||||
expect(Object.keys(mockTabLayouts)).toHaveLength(2);
|
||||
expect(Object.keys(result)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should handle multiple new tabs', () => {
|
||||
let result = createEmptyTabLayout(mockTabLayouts, 'tab-3');
|
||||
result = createEmptyTabLayout(result, 'tab-4');
|
||||
result = createEmptyTabLayout(result, 'tab-5');
|
||||
|
||||
expect(Object.keys(result)).toHaveLength(5);
|
||||
expect(result['tab-3'].desktop).toEqual([]);
|
||||
expect(result['tab-4'].desktop).toEqual([]);
|
||||
expect(result['tab-5'].desktop).toEqual([]);
|
||||
});
|
||||
|
||||
it('should create consistent structure for desktop and mobile', () => {
|
||||
const result = createEmptyTabLayout(mockTabLayouts, 'new-tab');
|
||||
|
||||
expect(result['new-tab']).toHaveProperty('desktop');
|
||||
expect(result['new-tab']).toHaveProperty('mobile');
|
||||
expect(Array.isArray(result['new-tab'].desktop)).toBe(true);
|
||||
expect(Array.isArray(result['new-tab'].mobile)).toBe(true);
|
||||
});
|
||||
});
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
import { type TabLayouts } from '../../states/pageLayoutCurrentLayoutsState';
|
||||
import { createUpdatedTabLayouts } from '../createUpdatedTabLayouts';
|
||||
|
||||
describe('createUpdatedTabLayouts', () => {
|
||||
const mockTabLayouts: TabLayouts = {
|
||||
'tab-1': {
|
||||
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2 }],
|
||||
mobile: [{ i: 'widget-1', x: 0, y: 0, w: 1, h: 2 }],
|
||||
},
|
||||
'tab-2': {
|
||||
desktop: [],
|
||||
mobile: [],
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
expect(result['tab-1'].desktop).toHaveLength(2);
|
||||
expect(result['tab-1'].desktop[1]).toEqual(newLayout);
|
||||
expect(result['tab-1'].mobile).toHaveLength(2);
|
||||
expect(result['tab-1'].mobile[1]).toEqual({ ...newLayout, w: 1, x: 0 });
|
||||
});
|
||||
|
||||
it('should add layout to empty tab', () => {
|
||||
const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-2', newLayout);
|
||||
|
||||
expect(result['tab-2'].desktop).toHaveLength(1);
|
||||
expect(result['tab-2'].desktop[0]).toEqual(newLayout);
|
||||
expect(result['tab-2'].mobile).toHaveLength(1);
|
||||
expect(result['tab-2'].mobile[0]).toEqual({ ...newLayout, w: 1, x: 0 });
|
||||
});
|
||||
|
||||
it('should create new tab entry if tab does not exist', () => {
|
||||
const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-3', newLayout);
|
||||
|
||||
expect(result['tab-3']).toBeDefined();
|
||||
expect(result['tab-3'].desktop).toHaveLength(1);
|
||||
expect(result['tab-3'].desktop[0]).toEqual(newLayout);
|
||||
expect(result['tab-3'].mobile).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not modify other tabs', () => {
|
||||
const result = createUpdatedTabLayouts(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 mobileLayout =
|
||||
result['tab-1'].mobile[result['tab-1'].mobile.length - 1];
|
||||
expect(mobileLayout.w).toBe(1);
|
||||
expect(mobileLayout.x).toBe(0);
|
||||
expect(mobileLayout.y).toBe(wideLayout.y);
|
||||
expect(mobileLayout.h).toBe(wideLayout.h);
|
||||
});
|
||||
|
||||
it('should return a new object without mutating the original', () => {
|
||||
const originalLayouts = JSON.parse(JSON.stringify(mockTabLayouts));
|
||||
const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout);
|
||||
|
||||
expect(result).not.toBe(mockTabLayouts);
|
||||
expect(mockTabLayouts).toEqual(originalLayouts);
|
||||
expect(result['tab-1'].desktop).toHaveLength(2);
|
||||
expect(mockTabLayouts['tab-1'].desktop).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw an error for malformed tab layouts', () => {
|
||||
const malformedLayouts: TabLayouts = {
|
||||
'tab-1': {} as any,
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
createUpdatedTabLayouts(malformedLayouts, 'tab-1', newLayout);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { generateCellId } from '../generateCellId';
|
||||
|
||||
describe('generateCellId', () => {
|
||||
it('should generate cell ID with correct format', () => {
|
||||
expect(generateCellId(3, 5)).toBe('cell-3-5');
|
||||
});
|
||||
|
||||
it('should handle zero values', () => {
|
||||
expect(generateCellId(0, 0)).toBe('cell-0-0');
|
||||
});
|
||||
|
||||
it('should handle large numbers', () => {
|
||||
expect(generateCellId(100, 200)).toBe('cell-100-200');
|
||||
});
|
||||
});
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { getDefaultWidgetPosition } from '../getDefaultWidgetPosition';
|
||||
|
||||
describe('getDefaultWidgetPosition', () => {
|
||||
it('should return dragged area when provided', () => {
|
||||
const draggedArea = { x: 2, y: 3, w: 4, h: 5 };
|
||||
const defaultSize = { w: 2, h: 2 };
|
||||
|
||||
expect(getDefaultWidgetPosition(draggedArea, defaultSize)).toEqual(
|
||||
draggedArea,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return default position with size when no dragged area', () => {
|
||||
const defaultSize = { w: 3, h: 4 };
|
||||
|
||||
expect(getDefaultWidgetPosition(null, defaultSize)).toEqual({
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 3,
|
||||
h: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { parseCellIdToCoordinates } from '../parseCellIdToCoordinates';
|
||||
|
||||
describe('parseCellIdToCoordinates', () => {
|
||||
it('should parse cell ID correctly', () => {
|
||||
expect(parseCellIdToCoordinates('cell-3-5')).toEqual({ col: 3, row: 5 });
|
||||
});
|
||||
|
||||
it('should handle zero coordinates', () => {
|
||||
expect(parseCellIdToCoordinates('cell-0-0')).toEqual({ col: 0, row: 0 });
|
||||
});
|
||||
|
||||
it('should handle double-digit coordinates', () => {
|
||||
expect(parseCellIdToCoordinates('cell-12-25')).toEqual({
|
||||
col: 12,
|
||||
row: 25,
|
||||
});
|
||||
});
|
||||
});
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
import { WidgetType } from '../../mocks/mockWidgets';
|
||||
import { type PageLayoutTab } from '../../states/savedPageLayoutsState';
|
||||
import { removeWidgetFromTab } from '../removeWidgetFromTab';
|
||||
|
||||
describe('removeWidgetFromTab', () => {
|
||||
const mockTabs: PageLayoutTab[] = [
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
pageLayoutId: 'layout-1',
|
||||
widgets: [
|
||||
{
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Widget 1',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
|
||||
objectMetadataId: null,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'widget-2',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: 'Widget 2',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 2, column: 0, rowSpan: 2, columnSpan: 2 },
|
||||
objectMetadataId: null,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'tab-2',
|
||||
title: 'Tab 2',
|
||||
position: 1,
|
||||
pageLayoutId: 'layout-1',
|
||||
widgets: [
|
||||
{
|
||||
id: 'widget-3',
|
||||
pageLayoutTabId: 'tab-2',
|
||||
title: 'Widget 3',
|
||||
type: WidgetType.IFRAME,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
|
||||
objectMetadataId: null,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
it('should remove widget from the correct tab', () => {
|
||||
const result = removeWidgetFromTab(mockTabs, 'tab-1', 'widget-1');
|
||||
|
||||
expect(result[0].widgets).toHaveLength(1);
|
||||
expect(result[0].widgets[0].id).toBe('widget-2');
|
||||
expect(result[1].widgets).toHaveLength(1);
|
||||
expect(result[1].widgets[0].id).toBe('widget-3');
|
||||
});
|
||||
|
||||
it('should not affect other tabs', () => {
|
||||
const result = removeWidgetFromTab(mockTabs, 'tab-1', 'widget-1');
|
||||
|
||||
expect(result[1]).toEqual(mockTabs[1]);
|
||||
expect(result[1].widgets).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should handle removing non-existent widget gracefully', () => {
|
||||
const result = removeWidgetFromTab(mockTabs, 'tab-1', 'non-existent');
|
||||
|
||||
expect(result[0].widgets).toHaveLength(2);
|
||||
expect(result[0].widgets).toEqual(mockTabs[0].widgets);
|
||||
});
|
||||
|
||||
it('should handle removing from non-existent tab gracefully', () => {
|
||||
const result = removeWidgetFromTab(mockTabs, 'non-existent', 'widget-1');
|
||||
|
||||
expect(result).toEqual(mockTabs);
|
||||
});
|
||||
|
||||
it('should remove all widgets if called multiple times', () => {
|
||||
let result = removeWidgetFromTab(mockTabs, 'tab-1', 'widget-1');
|
||||
result = removeWidgetFromTab(result, 'tab-1', 'widget-2');
|
||||
|
||||
expect(result[0].widgets).toHaveLength(0);
|
||||
expect(result[1].widgets).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should return a new array without mutating the original', () => {
|
||||
const originalTabs = structuredClone(mockTabs);
|
||||
const result = removeWidgetFromTab(mockTabs, 'tab-1', 'widget-1');
|
||||
|
||||
expect(result).not.toBe(mockTabs);
|
||||
expect(mockTabs).toEqual(originalTabs);
|
||||
expect(result[0].widgets).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
import { type TabLayouts } from '../../states/pageLayoutCurrentLayoutsState';
|
||||
import { removeWidgetLayoutFromTab } from '../removeWidgetLayoutFromTab';
|
||||
|
||||
describe('removeWidgetLayoutFromTab', () => {
|
||||
const mockTabLayouts: TabLayouts = {
|
||||
'tab-1': {
|
||||
desktop: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2 },
|
||||
{ i: 'widget-2', x: 2, y: 0, w: 3, h: 3 },
|
||||
{ i: 'widget-3', x: 5, y: 0, w: 2, h: 2 },
|
||||
],
|
||||
mobile: [
|
||||
{ i: 'widget-1', x: 0, y: 0, w: 1, h: 2 },
|
||||
{ i: 'widget-2', x: 0, y: 2, w: 1, h: 3 },
|
||||
{ i: 'widget-3', x: 0, y: 5, w: 1, h: 2 },
|
||||
],
|
||||
},
|
||||
'tab-2': {
|
||||
desktop: [{ i: 'widget-4', x: 0, y: 0, w: 4, h: 4 }],
|
||||
mobile: [{ i: 'widget-4', x: 0, y: 0, w: 1, h: 4 }],
|
||||
},
|
||||
};
|
||||
|
||||
it('should remove widget layout from the correct tab', () => {
|
||||
const result = removeWidgetLayoutFromTab(
|
||||
mockTabLayouts,
|
||||
'tab-1',
|
||||
'widget-2',
|
||||
);
|
||||
|
||||
expect(result['tab-1'].desktop).toHaveLength(2);
|
||||
expect(result['tab-1'].desktop.map((l) => l.i)).toEqual([
|
||||
'widget-1',
|
||||
'widget-3',
|
||||
]);
|
||||
expect(result['tab-1'].mobile).toHaveLength(2);
|
||||
expect(result['tab-1'].mobile.map((l) => l.i)).toEqual([
|
||||
'widget-1',
|
||||
'widget-3',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not affect other tabs', () => {
|
||||
const result = removeWidgetLayoutFromTab(
|
||||
mockTabLayouts,
|
||||
'tab-1',
|
||||
'widget-1',
|
||||
);
|
||||
|
||||
expect(result['tab-2']).toEqual(mockTabLayouts['tab-2']);
|
||||
expect(result['tab-2'].desktop).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should handle non-existent tab gracefully', () => {
|
||||
const result = removeWidgetLayoutFromTab(
|
||||
mockTabLayouts,
|
||||
'non-existent',
|
||||
'widget-1',
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockTabLayouts);
|
||||
});
|
||||
|
||||
it('should handle non-existent widget gracefully', () => {
|
||||
const result = removeWidgetLayoutFromTab(
|
||||
mockTabLayouts,
|
||||
'tab-1',
|
||||
'non-existent',
|
||||
);
|
||||
|
||||
expect(result['tab-1'].desktop).toHaveLength(3);
|
||||
expect(result['tab-1']).toEqual(mockTabLayouts['tab-1']);
|
||||
});
|
||||
|
||||
it('should remove all widgets from a tab', () => {
|
||||
let result = removeWidgetLayoutFromTab(mockTabLayouts, 'tab-1', 'widget-1');
|
||||
result = removeWidgetLayoutFromTab(result, 'tab-1', 'widget-2');
|
||||
result = removeWidgetLayoutFromTab(result, 'tab-1', 'widget-3');
|
||||
|
||||
expect(result['tab-1'].desktop).toHaveLength(0);
|
||||
expect(result['tab-1'].mobile).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle empty tab layouts', () => {
|
||||
const emptyLayouts: TabLayouts = {
|
||||
'tab-1': {
|
||||
desktop: [],
|
||||
mobile: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = removeWidgetLayoutFromTab(emptyLayouts, 'tab-1', 'widget-1');
|
||||
|
||||
expect(result['tab-1'].desktop).toHaveLength(0);
|
||||
expect(result['tab-1'].mobile).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return a new object without mutating the original', () => {
|
||||
const originalLayouts = JSON.parse(JSON.stringify(mockTabLayouts));
|
||||
const result = removeWidgetLayoutFromTab(
|
||||
mockTabLayouts,
|
||||
'tab-1',
|
||||
'widget-1',
|
||||
);
|
||||
|
||||
expect(result).not.toBe(mockTabLayouts);
|
||||
expect(mockTabLayouts).toEqual(originalLayouts);
|
||||
expect(result['tab-1'].desktop).toHaveLength(2);
|
||||
expect(mockTabLayouts['tab-1'].desktop).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should remove widget from both desktop and mobile layouts', () => {
|
||||
const result = removeWidgetLayoutFromTab(
|
||||
mockTabLayouts,
|
||||
'tab-1',
|
||||
'widget-2',
|
||||
);
|
||||
|
||||
const desktopIds = result['tab-1'].desktop.map((l) => l.i);
|
||||
const mobileIds = result['tab-1'].mobile.map((l) => l.i);
|
||||
|
||||
expect(desktopIds).not.toContain('widget-2');
|
||||
expect(mobileIds).not.toContain('widget-2');
|
||||
expect(desktopIds).toEqual(mobileIds);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import {
|
||||
type PageLayoutTab,
|
||||
type PageLayoutWidget,
|
||||
} from '../states/savedPageLayoutsState';
|
||||
|
||||
export const addWidgetToTab = (
|
||||
tabs: PageLayoutTab[],
|
||||
activeTabId: string,
|
||||
newWidget: PageLayoutWidget,
|
||||
): PageLayoutTab[] => {
|
||||
return tabs.map((tab) => {
|
||||
if (tab.id === activeTabId) {
|
||||
return {
|
||||
...tab,
|
||||
widgets: [...tab.widgets, newWidget],
|
||||
};
|
||||
}
|
||||
return tab;
|
||||
});
|
||||
};
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { parseCellIdToCoordinates } from './parseCellIdToCoordinates';
|
||||
|
||||
export type GridBounds = {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
};
|
||||
|
||||
export const calculateGridBoundsFromSelectedCells = (
|
||||
selectedCellIds: string[],
|
||||
): GridBounds | null => {
|
||||
if (selectedCellIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cellCoords = selectedCellIds.map(parseCellIdToCoordinates);
|
||||
|
||||
const minCol = Math.min(...cellCoords.map((c) => c.col));
|
||||
const maxCol = Math.max(...cellCoords.map((c) => c.col));
|
||||
const minRow = Math.min(...cellCoords.map((c) => c.row));
|
||||
const maxRow = Math.max(...cellCoords.map((c) => c.row));
|
||||
|
||||
return {
|
||||
x: minCol,
|
||||
y: minRow,
|
||||
w: maxCol - minCol + 1,
|
||||
h: maxRow - minRow + 1,
|
||||
};
|
||||
};
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
import { GRID_BUFFER_ROWS } from '../constants/GridBufferRows';
|
||||
import { GRID_MIN_ROWS } from '../constants/GridMinRows';
|
||||
|
||||
export const calculateTotalGridRows = (
|
||||
layouts: Layouts,
|
||||
minRows = GRID_MIN_ROWS,
|
||||
bufferRows = GRID_BUFFER_ROWS,
|
||||
): number => {
|
||||
const allLayouts = [...(layouts.desktop || []), ...(layouts.mobile || [])];
|
||||
|
||||
const contentRows =
|
||||
allLayouts.length === 0
|
||||
? 0
|
||||
: Math.max(...allLayouts.map((item) => item.y + item.h));
|
||||
|
||||
return Math.max(minRows, contentRows + bufferRows);
|
||||
};
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
|
||||
export const convertLayoutsToWidgets = (
|
||||
widgets: PageLayoutWidget[],
|
||||
layouts: Layouts,
|
||||
): PageLayoutWidget[] => {
|
||||
const activeLayouts = layouts.desktop || layouts.mobile || [];
|
||||
|
||||
return widgets.map((widget) => {
|
||||
const layout = activeLayouts.find((l) => l.i === widget.id);
|
||||
return {
|
||||
...widget,
|
||||
gridPosition: {
|
||||
row: layout?.y ?? 0,
|
||||
column: layout?.x ?? 0,
|
||||
rowSpan: layout?.h ?? 2,
|
||||
columnSpan: layout?.w ?? 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import { type TabLayouts } from '../states/pageLayoutCurrentLayoutsState';
|
||||
|
||||
export const createEmptyTabLayout = (
|
||||
allTabLayouts: TabLayouts,
|
||||
tabId: string,
|
||||
): TabLayouts => {
|
||||
return {
|
||||
...allTabLayouts,
|
||||
[tabId]: { desktop: [], mobile: [] },
|
||||
};
|
||||
};
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { type TabLayouts } from '../states/pageLayoutCurrentLayoutsState';
|
||||
|
||||
export const createUpdatedTabLayouts = (
|
||||
allTabLayouts: TabLayouts,
|
||||
activeTabId: string,
|
||||
newLayout: { i: string; x: number; y: number; w: number; h: number },
|
||||
): TabLayouts => {
|
||||
const currentTabLayouts = allTabLayouts[activeTabId] || {
|
||||
desktop: [],
|
||||
mobile: [],
|
||||
};
|
||||
|
||||
return {
|
||||
...allTabLayouts,
|
||||
[activeTabId]: {
|
||||
desktop: [...currentTabLayouts.desktop, newLayout],
|
||||
mobile: [...currentTabLayouts.mobile, { ...newLayout, w: 1, x: 0 }],
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export const generateCellId = (col: number, row: number): string => {
|
||||
return `cell-${col}-${row}`;
|
||||
};
|
||||
@@ -1,137 +0,0 @@
|
||||
import { GraphType } from '../mocks/mockWidgets';
|
||||
|
||||
export const getDefaultWidgetData = (graphType: GraphType) => {
|
||||
switch (graphType) {
|
||||
case GraphType.NUMBER:
|
||||
return {
|
||||
value: '1,234',
|
||||
trendPercentage: 15.2,
|
||||
};
|
||||
|
||||
case GraphType.GAUGE:
|
||||
return {
|
||||
value: 0.7,
|
||||
min: 0,
|
||||
max: 1,
|
||||
label: 'Progress',
|
||||
};
|
||||
|
||||
case GraphType.PIE:
|
||||
return {
|
||||
items: [
|
||||
{ id: 'segment1', value: 35, label: 'Segment A' },
|
||||
{ id: 'segment2', value: 28, label: 'Segment B' },
|
||||
{ id: 'segment3', value: 20, label: 'Segment C' },
|
||||
{ id: 'segment4', value: 17, label: 'Segment D' },
|
||||
],
|
||||
};
|
||||
|
||||
case GraphType.BAR:
|
||||
return {
|
||||
items: [
|
||||
{ category: 'Jan', value: 45 },
|
||||
{ category: 'Feb', value: 52 },
|
||||
{ category: 'Mar', value: 48 },
|
||||
{ category: 'Apr', value: 61 },
|
||||
{ category: 'May', value: 55 },
|
||||
],
|
||||
indexBy: 'category',
|
||||
keys: ['value'],
|
||||
seriesLabels: { value: 'Value' },
|
||||
layout: 'vertical' as const,
|
||||
};
|
||||
|
||||
case GraphType.LINE:
|
||||
return {
|
||||
series: [
|
||||
{
|
||||
id: 'revenue',
|
||||
label: 'Revenue',
|
||||
color: 'blue',
|
||||
data: [
|
||||
{ x: 0, y: 30 },
|
||||
{ x: 1, y: 50 },
|
||||
{ x: 2, y: 45 },
|
||||
{ x: 3, y: 70 },
|
||||
{ x: 4, y: 65 },
|
||||
{ x: 5, y: 80 },
|
||||
{ x: 6, y: 75 },
|
||||
{ x: 7, y: 85 },
|
||||
],
|
||||
enableArea: true,
|
||||
},
|
||||
{
|
||||
id: 'costs',
|
||||
label: 'Costs',
|
||||
color: 'red',
|
||||
data: [
|
||||
{ x: 0, y: 60 },
|
||||
{ x: 1, y: 45 },
|
||||
{ x: 2, y: 55 },
|
||||
{ x: 3, y: 40 },
|
||||
{ x: 4, y: 60 },
|
||||
{ x: 5, y: 50 },
|
||||
{ x: 6, y: 70 },
|
||||
{ x: 7, y: 65 },
|
||||
],
|
||||
enableArea: true,
|
||||
},
|
||||
{
|
||||
id: 'profit',
|
||||
label: 'Profit',
|
||||
color: 'turquoise',
|
||||
data: [
|
||||
{ x: 0, y: 45 },
|
||||
{ x: 1, y: 60 },
|
||||
{ x: 2, y: 35 },
|
||||
{ x: 3, y: 55 },
|
||||
{ x: 4, y: 50 },
|
||||
{ x: 5, y: 65 },
|
||||
{ x: 6, y: 40 },
|
||||
{ x: 7, y: 75 },
|
||||
],
|
||||
enableArea: true,
|
||||
},
|
||||
],
|
||||
enableArea: true,
|
||||
showLegend: true,
|
||||
showGrid: true,
|
||||
enablePoints: false,
|
||||
curve: 'monotoneX',
|
||||
displayType: 'shortNumber',
|
||||
prefix: '$',
|
||||
};
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const getWidgetTitle = (graphType: GraphType, index: number): string => {
|
||||
const baseNames: Record<GraphType, string> = {
|
||||
[GraphType.NUMBER]: 'Number',
|
||||
[GraphType.GAUGE]: 'Gauge',
|
||||
[GraphType.PIE]: 'Pie Chart',
|
||||
[GraphType.BAR]: 'Bar Chart',
|
||||
[GraphType.LINE]: 'Line Chart',
|
||||
};
|
||||
|
||||
return `${baseNames[graphType] || 'Widget'} ${index + 1}`;
|
||||
};
|
||||
|
||||
export const getWidgetSize = (graphType: GraphType) => {
|
||||
switch (graphType) {
|
||||
case GraphType.NUMBER:
|
||||
return { w: 3, h: 2 };
|
||||
case GraphType.GAUGE:
|
||||
return { w: 3, h: 3 };
|
||||
case GraphType.PIE:
|
||||
return { w: 4, h: 4 };
|
||||
case GraphType.BAR:
|
||||
return { w: 6, h: 4 };
|
||||
case GraphType.LINE:
|
||||
return { w: 6, h: 10 };
|
||||
default:
|
||||
return { w: 4, h: 4 };
|
||||
}
|
||||
};
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { type GridBounds } from './calculateGridBoundsFromSelectedCells';
|
||||
|
||||
export const getDefaultWidgetPosition = (
|
||||
draggedArea: GridBounds | null,
|
||||
defaultSize: { w: number; h: number },
|
||||
): GridBounds => {
|
||||
if (draggedArea !== null) {
|
||||
return draggedArea;
|
||||
}
|
||||
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: defaultSize.w,
|
||||
h: defaultSize.h,
|
||||
};
|
||||
};
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
export type CellCoordinate = {
|
||||
col: number;
|
||||
row: number;
|
||||
};
|
||||
|
||||
export const parseCellIdToCoordinates = (cellId: string): CellCoordinate => {
|
||||
const [col, row] = cellId.split('-').slice(1).map(Number);
|
||||
return { col, row };
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { type PageLayoutTab } from '../states/savedPageLayoutsState';
|
||||
|
||||
export const removeWidgetFromTab = (
|
||||
tabs: PageLayoutTab[],
|
||||
tabId: string,
|
||||
widgetId: string,
|
||||
): PageLayoutTab[] => {
|
||||
return tabs.map((tab) => {
|
||||
if (tab.id === tabId) {
|
||||
return {
|
||||
...tab,
|
||||
widgets: tab.widgets.filter((w) => w.id !== widgetId),
|
||||
};
|
||||
}
|
||||
return tab;
|
||||
});
|
||||
};
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
import { type TabLayouts } from '../states/pageLayoutCurrentLayoutsState';
|
||||
|
||||
export const removeWidgetLayoutFromTab = (
|
||||
allTabLayouts: TabLayouts,
|
||||
tabId: string,
|
||||
widgetId: string,
|
||||
): TabLayouts => {
|
||||
if (!allTabLayouts[tabId]) {
|
||||
return allTabLayouts;
|
||||
}
|
||||
|
||||
const currentTabLayouts = allTabLayouts[tabId];
|
||||
return {
|
||||
...allTabLayouts,
|
||||
[tabId]: {
|
||||
desktop: currentTabLayouts.desktop.filter(
|
||||
(layout) => layout.i !== widgetId,
|
||||
),
|
||||
mobile: currentTabLayouts.mobile.filter(
|
||||
(layout) => layout.i !== widgetId,
|
||||
),
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user