[Page Layout] - Review Refactor (#14348)
addressing the review on https://github.com/twentyhq/twenty/pull/14318#pullrequestreview-3191930203
This commit is contained in:
+9
-9
@@ -1,7 +1,7 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { usePageLayoutWidgetCreate } from '@/settings/page-layout/hooks/usePageLayoutWidgetCreate';
|
||||
import { useCreatePageLayoutWidget } from '@/settings/page-layout/hooks/useCreatePageLayoutWidget';
|
||||
import {
|
||||
GraphSubType,
|
||||
GraphType,
|
||||
WidgetType,
|
||||
} from '@/settings/page-layout/mocks/mockWidgets';
|
||||
import styled from '@emotion/styled';
|
||||
@@ -30,22 +30,22 @@ const StyledSectionTitle = styled.div`
|
||||
|
||||
const graphTypeOptions = [
|
||||
{
|
||||
type: GraphSubType.BAR,
|
||||
type: GraphType.BAR,
|
||||
icon: IconChartBar,
|
||||
title: 'Bar Chart',
|
||||
},
|
||||
{
|
||||
type: GraphSubType.PIE,
|
||||
type: GraphType.PIE,
|
||||
icon: IconChartPie,
|
||||
title: 'Pie Chart',
|
||||
},
|
||||
{
|
||||
type: GraphSubType.GAUGE,
|
||||
type: GraphType.GAUGE,
|
||||
icon: IconGauge,
|
||||
title: 'Gauge',
|
||||
},
|
||||
{
|
||||
type: GraphSubType.NUMBER,
|
||||
type: GraphType.NUMBER,
|
||||
icon: IconNumber,
|
||||
title: 'Number',
|
||||
},
|
||||
@@ -53,10 +53,10 @@ const graphTypeOptions = [
|
||||
|
||||
export const CommandMenuPageLayoutGraphTypeSelect = () => {
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
const { handleCreateWidget } = usePageLayoutWidgetCreate();
|
||||
const { createPageLayoutWidget } = useCreatePageLayoutWidget();
|
||||
|
||||
const handleSelectGraphType = (graphType: GraphSubType) => {
|
||||
handleCreateWidget(WidgetType.GRAPH, graphType);
|
||||
const handleSelectGraphType = (graphType: GraphType) => {
|
||||
createPageLayoutWidget(WidgetType.GRAPH, graphType);
|
||||
closeCommandMenu();
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { useCreatePageLayoutIframeWidget } from '@/settings/page-layout/hooks/useCreatePageLayoutIframeWidget';
|
||||
import { usePageLayoutWidgetUpdate } from '@/settings/page-layout/hooks/usePageLayoutWidgetUpdate';
|
||||
import { useUpdatePageLayoutWidget } from '@/settings/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { pageLayoutDraftState } from '@/settings/page-layout/states/pageLayoutDraftState';
|
||||
import { pageLayoutEditingWidgetIdState } from '@/settings/page-layout/states/pageLayoutEditingWidgetIdState';
|
||||
import styled from '@emotion/styled';
|
||||
@@ -34,7 +34,7 @@ const StyledButtonContainer = styled.div`
|
||||
export const CommandMenuPageLayoutIframeConfig = () => {
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
const { createPageLayoutIframeWidget } = useCreatePageLayoutIframeWidget();
|
||||
const { handleUpdateWidget } = usePageLayoutWidgetUpdate();
|
||||
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget();
|
||||
const [pageLayoutEditingWidgetId, setPageLayoutEditingWidgetId] =
|
||||
useRecoilState(pageLayoutEditingWidgetIdState);
|
||||
const pageLayoutDraft = useRecoilValue(pageLayoutDraftState);
|
||||
@@ -77,7 +77,7 @@ export const CommandMenuPageLayoutIframeConfig = () => {
|
||||
}
|
||||
|
||||
if (isEditMode && pageLayoutEditingWidgetId !== null) {
|
||||
handleUpdateWidget(pageLayoutEditingWidgetId, {
|
||||
updatePageLayoutWidget(pageLayoutEditingWidgetId, {
|
||||
title: title.trim(),
|
||||
configuration: {
|
||||
...editingWidget?.configuration,
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { GraphWidgetBarChart } from '@/dashboards/widgets/graph/components/GraphWidgetBarChart';
|
||||
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 { 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}`}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
-2
@@ -42,7 +42,6 @@ export const PageLayoutInitializationEffect = ({
|
||||
set(pageLayoutDraftState, {
|
||||
name: layout.name,
|
||||
type: layout.type,
|
||||
workspaceId: layout.workspaceId,
|
||||
objectMetadataId: layout.objectMetadataId,
|
||||
tabs: layout.tabs,
|
||||
});
|
||||
@@ -82,7 +81,6 @@ export const PageLayoutInitializationEffect = ({
|
||||
set(pageLayoutDraftState, {
|
||||
name: '',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
workspaceId: undefined,
|
||||
objectMetadataId: null,
|
||||
tabs: [defaultTab],
|
||||
});
|
||||
|
||||
+7
-4
@@ -1,14 +1,17 @@
|
||||
import { IframeWidget } from '@/dashboards/widgets/iframe/components/IframeWidget';
|
||||
import { isString } from '@sniptt/guards';
|
||||
import { type ReactNode } from 'react';
|
||||
import { WidgetType } from '../mocks/mockWidgets';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
import { renderGraphWidget } from './graphRegistry';
|
||||
import { GraphWidgetRenderer } from './GraphWidgetRenderer';
|
||||
|
||||
export const renderWidget = (widget: PageLayoutWidget): ReactNode => {
|
||||
type WidgetRendererProps = {
|
||||
widget: PageLayoutWidget;
|
||||
};
|
||||
|
||||
export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
switch (widget.type) {
|
||||
case WidgetType.GRAPH:
|
||||
return renderGraphWidget(widget);
|
||||
return <GraphWidgetRenderer widget={widget} />;
|
||||
|
||||
case WidgetType.IFRAME: {
|
||||
const url = widget.configuration?.url;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID =
|
||||
'settings-page-layout-tabs';
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
||||
+12
-12
@@ -2,13 +2,13 @@ import { pageLayoutCurrentLayoutsState } from '@/settings/page-layout/states/pag
|
||||
import { pageLayoutDraftState } from '@/settings/page-layout/states/pageLayoutDraftState';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
import { usePageLayoutTabCreate } from '../usePageLayoutTabCreate';
|
||||
import { useCreatePageLayoutTab } from '../useCreatePageLayoutTab';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('usePageLayoutTabCreate', () => {
|
||||
describe('useCreatePageLayoutTab', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -18,7 +18,7 @@ describe('usePageLayoutTabCreate', () => {
|
||||
uuidModule.v4.mockReturnValue('mock-uuid');
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createTab: usePageLayoutTabCreate(),
|
||||
createTab: useCreatePageLayoutTab(),
|
||||
pageLayoutCurrentLayouts: useRecoilValue(pageLayoutCurrentLayoutsState),
|
||||
pageLayoutDraft: useRecoilValue(pageLayoutDraftState),
|
||||
}),
|
||||
@@ -29,7 +29,7 @@ describe('usePageLayoutTabCreate', () => {
|
||||
|
||||
let newTabId: string;
|
||||
act(() => {
|
||||
newTabId = result.current.createTab.handleCreateTab();
|
||||
newTabId = result.current.createTab.createPageLayoutTab();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutDraft.tabs[0].id).toBe('tab-mock-uuid');
|
||||
@@ -50,7 +50,7 @@ describe('usePageLayoutTabCreate', () => {
|
||||
uuidModule.v4.mockReturnValue('mock-uuid');
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createTab: usePageLayoutTabCreate(),
|
||||
createTab: useCreatePageLayoutTab(),
|
||||
pageLayoutDraft: useRecoilValue(pageLayoutDraftState),
|
||||
}),
|
||||
{
|
||||
@@ -59,7 +59,7 @@ describe('usePageLayoutTabCreate', () => {
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.createTab.handleCreateTab('Custom Tab Name');
|
||||
result.current.createTab.createPageLayoutTab('Custom Tab Name');
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutDraft.tabs[0].title).toBe(
|
||||
@@ -74,7 +74,7 @@ describe('usePageLayoutTabCreate', () => {
|
||||
.mockReturnValueOnce('mock-uuid-2');
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createTab: usePageLayoutTabCreate(),
|
||||
createTab: useCreatePageLayoutTab(),
|
||||
pageLayoutDraft: useRecoilValue(pageLayoutDraftState),
|
||||
}),
|
||||
{
|
||||
@@ -83,11 +83,11 @@ describe('usePageLayoutTabCreate', () => {
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.createTab.handleCreateTab();
|
||||
result.current.createTab.createPageLayoutTab();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.createTab.handleCreateTab();
|
||||
result.current.createTab.createPageLayoutTab();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutDraft.tabs).toHaveLength(2);
|
||||
@@ -104,7 +104,7 @@ describe('usePageLayoutTabCreate', () => {
|
||||
.mockReturnValueOnce('mock-uuid-2');
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createTab: usePageLayoutTabCreate(),
|
||||
createTab: useCreatePageLayoutTab(),
|
||||
pageLayoutCurrentLayouts: useRecoilValue(pageLayoutCurrentLayoutsState),
|
||||
}),
|
||||
{
|
||||
@@ -114,12 +114,12 @@ describe('usePageLayoutTabCreate', () => {
|
||||
|
||||
let tabId1: string = '';
|
||||
act(() => {
|
||||
tabId1 = result.current.createTab.handleCreateTab();
|
||||
tabId1 = result.current.createTab.createPageLayoutTab();
|
||||
});
|
||||
|
||||
let tabId2: string = '';
|
||||
act(() => {
|
||||
tabId2 = result.current.createTab.handleCreateTab();
|
||||
tabId2 = result.current.createTab.createPageLayoutTab();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutCurrentLayouts[tabId1]).toEqual({
|
||||
+51
-23
@@ -1,20 +1,21 @@
|
||||
import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '@/settings/page-layout/constants/SettingsPageLayoutTabsInstanceId';
|
||||
import {
|
||||
GraphSubType,
|
||||
GraphType,
|
||||
WidgetType,
|
||||
} from '@/settings/page-layout/mocks/mockWidgets';
|
||||
import { pageLayoutCurrentLayoutsState } from '@/settings/page-layout/states/pageLayoutCurrentLayoutsState';
|
||||
import { pageLayoutCurrentTabIdForCreationState } from '@/settings/page-layout/states/pageLayoutCurrentTabIdForCreation';
|
||||
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 { usePageLayoutWidgetCreate } from '../usePageLayoutWidgetCreate';
|
||||
import { useCreatePageLayoutWidget } from '../useCreatePageLayoutWidget';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid'),
|
||||
}));
|
||||
|
||||
describe('usePageLayoutWidgetCreate', () => {
|
||||
describe('useCreatePageLayoutWidget', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -23,7 +24,9 @@ describe('usePageLayoutWidgetCreate', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setActiveTabId = useSetRecoilState(
|
||||
pageLayoutCurrentTabIdForCreationState,
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID,
|
||||
}),
|
||||
);
|
||||
const setPageLayoutDraft = useSetRecoilState(pageLayoutDraftState);
|
||||
const pageLayoutDraft = useRecoilValue(pageLayoutDraftState);
|
||||
@@ -31,7 +34,7 @@ describe('usePageLayoutWidgetCreate', () => {
|
||||
const pageLayoutCurrentLayouts = useRecoilValue(
|
||||
pageLayoutCurrentLayoutsState,
|
||||
);
|
||||
const createWidget = usePageLayoutWidgetCreate();
|
||||
const createWidget = useCreatePageLayoutWidget();
|
||||
return {
|
||||
setActiveTabId,
|
||||
setPageLayoutDraft,
|
||||
@@ -49,7 +52,6 @@ describe('usePageLayoutWidgetCreate', () => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
workspaceId: undefined,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
@@ -68,9 +70,9 @@ describe('usePageLayoutWidgetCreate', () => {
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.createWidget.handleCreateWidget(
|
||||
result.current.createWidget.createPageLayoutWidget(
|
||||
WidgetType.GRAPH,
|
||||
GraphSubType.BAR,
|
||||
GraphType.BAR,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -89,12 +91,22 @@ describe('usePageLayoutWidgetCreate', () => {
|
||||
() => {
|
||||
const setPageLayoutDraft = useSetRecoilState(pageLayoutDraftState);
|
||||
const setActiveTabId = useSetRecoilState(
|
||||
pageLayoutCurrentTabIdForCreationState,
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID,
|
||||
}),
|
||||
);
|
||||
const createWidget = usePageLayoutWidgetCreate();
|
||||
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,
|
||||
};
|
||||
},
|
||||
@@ -107,7 +119,6 @@ describe('usePageLayoutWidgetCreate', () => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
workspaceId: undefined,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
@@ -126,24 +137,41 @@ describe('usePageLayoutWidgetCreate', () => {
|
||||
});
|
||||
|
||||
const graphTypes = [
|
||||
GraphSubType.NUMBER,
|
||||
GraphSubType.GAUGE,
|
||||
GraphSubType.PIE,
|
||||
GraphSubType.BAR,
|
||||
GraphType.NUMBER,
|
||||
GraphType.GAUGE,
|
||||
GraphType.PIE,
|
||||
GraphType.BAR,
|
||||
];
|
||||
|
||||
graphTypes.forEach((graphType) => {
|
||||
act(() => {
|
||||
result.current.createWidget.handleCreateWidget(
|
||||
result.current.createWidget.createPageLayoutWidget(
|
||||
WidgetType.GRAPH,
|
||||
graphType,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
expect(typeof result.current.createWidget.handleCreateWidget).toBe(
|
||||
'function',
|
||||
);
|
||||
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', () => {
|
||||
@@ -154,7 +182,7 @@ describe('usePageLayoutWidgetCreate', () => {
|
||||
const pageLayoutCurrentLayouts = useRecoilValue(
|
||||
pageLayoutCurrentLayoutsState,
|
||||
);
|
||||
const createWidget = usePageLayoutWidgetCreate();
|
||||
const createWidget = useCreatePageLayoutWidget();
|
||||
return { allWidgets, pageLayoutCurrentLayouts, createWidget };
|
||||
},
|
||||
{
|
||||
@@ -163,9 +191,9 @@ describe('usePageLayoutWidgetCreate', () => {
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.createWidget.handleCreateWidget(
|
||||
result.current.createWidget.createPageLayoutWidget(
|
||||
WidgetType.GRAPH,
|
||||
GraphSubType.BAR,
|
||||
GraphType.BAR,
|
||||
);
|
||||
});
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
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
@@ -0,0 +1,208 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
+2
-5
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
GraphSubType,
|
||||
GraphType,
|
||||
WidgetType,
|
||||
} from '@/settings/page-layout/mocks/mockWidgets';
|
||||
import { PageLayoutType } from '@/settings/page-layout/states/savedPageLayoutsState';
|
||||
@@ -26,7 +26,6 @@ describe('usePageLayoutDraftState', () => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: ' ',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
workspaceId: undefined,
|
||||
objectMetadataId: null,
|
||||
tabs: [],
|
||||
});
|
||||
@@ -45,7 +44,6 @@ describe('usePageLayoutDraftState', () => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: 'Updated Name',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
workspaceId: undefined,
|
||||
objectMetadataId: null,
|
||||
tabs: [],
|
||||
});
|
||||
@@ -65,7 +63,6 @@ describe('usePageLayoutDraftState', () => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
workspaceId: undefined,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
@@ -83,7 +80,7 @@ describe('usePageLayoutDraftState', () => {
|
||||
title: 'New Widget',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 2, column: 2, rowSpan: 2, columnSpan: 2 },
|
||||
configuration: { graphType: GraphSubType.BAR },
|
||||
configuration: { graphType: GraphType.BAR },
|
||||
data: {},
|
||||
objectMetadataId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { usePageLayoutDragSelection } from '../usePageLayoutDragSelection';
|
||||
|
||||
describe('usePageLayoutDragSelection', () => {
|
||||
it('should initialize with empty selected cells', () => {
|
||||
const { result } = renderHook(() => usePageLayoutDragSelection(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutSelectedCells).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should clear selected cells on drag start', () => {
|
||||
const { result } = renderHook(() => usePageLayoutDragSelection(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleDragSelectionChange('cell-1', true);
|
||||
result.current.handleDragSelectionChange('cell-2', true);
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutSelectedCells.size).toBe(2);
|
||||
|
||||
act(() => {
|
||||
result.current.handleDragSelectionStart();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutSelectedCells).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should add and remove cells during drag selection', () => {
|
||||
const { result } = renderHook(() => usePageLayoutDragSelection(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleDragSelectionChange('cell-1', true);
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutSelectedCells.has('cell-1')).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.handleDragSelectionChange('cell-2', true);
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutSelectedCells.size).toBe(2);
|
||||
|
||||
act(() => {
|
||||
result.current.handleDragSelectionChange('cell-1', false);
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutSelectedCells.has('cell-1')).toBe(false);
|
||||
expect(result.current.pageLayoutSelectedCells.size).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle drag selection end with selected cells', () => {
|
||||
const { result } = renderHook(() => usePageLayoutDragSelection(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleDragSelectionChange('0-0', true);
|
||||
result.current.handleDragSelectionChange('1-0', true);
|
||||
result.current.handleDragSelectionChange('0-1', true);
|
||||
result.current.handleDragSelectionChange('1-1', true);
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutSelectedCells.size).toBe(4);
|
||||
|
||||
act(() => {
|
||||
result.current.handleDragSelectionEnd();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutSelectedCells).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should handle drag selection end with no selected cells', () => {
|
||||
const { result } = renderHook(() => usePageLayoutDragSelection(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleDragSelectionEnd();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutSelectedCells).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should provide all required handler functions', () => {
|
||||
const { result } = renderHook(() => usePageLayoutDragSelection(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
expect(typeof result.current.handleDragSelectionStart).toBe('function');
|
||||
expect(typeof result.current.handleDragSelectionChange).toBe('function');
|
||||
expect(typeof result.current.handleDragSelectionEnd).toBe('function');
|
||||
});
|
||||
});
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { usePageLayoutWidgetDelete } from '../usePageLayoutWidgetDelete';
|
||||
|
||||
describe('usePageLayoutWidgetDelete', () => {
|
||||
it('should remove widget from all states', () => {
|
||||
const { result } = renderHook(() => usePageLayoutWidgetDelete(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleRemoveWidget('widget-1');
|
||||
});
|
||||
|
||||
expect(typeof result.current.handleRemoveWidget).toBe('function');
|
||||
});
|
||||
|
||||
it('should handle removing non-existent widget', () => {
|
||||
const { result } = renderHook(() => usePageLayoutWidgetDelete(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleRemoveWidget('non-existent-widget');
|
||||
});
|
||||
|
||||
expect(typeof result.current.handleRemoveWidget).toBe('function');
|
||||
});
|
||||
|
||||
it('should handle empty layouts', () => {
|
||||
const { result } = renderHook(() => usePageLayoutWidgetDelete(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleRemoveWidget('any-widget');
|
||||
});
|
||||
|
||||
expect(typeof result.current.handleRemoveWidget).toBe('function');
|
||||
});
|
||||
});
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
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
@@ -0,0 +1,22 @@
|
||||
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 };
|
||||
};
|
||||
+9
-6
@@ -1,8 +1,10 @@
|
||||
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 { pageLayoutCurrentTabIdForCreationState } from '../states/pageLayoutCurrentTabIdForCreation';
|
||||
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
import { pageLayoutDraggedAreaState } from '../states/pageLayoutDraggedAreaState';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
@@ -11,6 +13,11 @@ 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) => {
|
||||
@@ -21,10 +28,6 @@ export const useCreatePageLayoutIframeWidget = () => {
|
||||
.getLoadable(pageLayoutDraggedAreaState)
|
||||
.getValue();
|
||||
|
||||
const activeTabId = snapshot
|
||||
.getLoadable(pageLayoutCurrentTabIdForCreationState)
|
||||
.getValue();
|
||||
|
||||
if (!activeTabId) {
|
||||
return;
|
||||
}
|
||||
@@ -78,7 +81,7 @@ export const useCreatePageLayoutIframeWidget = () => {
|
||||
|
||||
set(pageLayoutDraggedAreaState, null);
|
||||
},
|
||||
[],
|
||||
[activeTabId],
|
||||
);
|
||||
|
||||
return { createPageLayoutIframeWidget };
|
||||
|
||||
+3
-3
@@ -5,8 +5,8 @@ import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
import { type PageLayoutTab } from '../states/savedPageLayoutsState';
|
||||
import { createEmptyTabLayout } from '../utils/createEmptyTabLayout';
|
||||
|
||||
export const usePageLayoutTabCreate = () => {
|
||||
const handleCreateTab = useRecoilCallback(
|
||||
export const useCreatePageLayoutTab = () => {
|
||||
const createPageLayoutTab = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(title?: string): string => {
|
||||
const pageLayoutDraft = snapshot
|
||||
@@ -41,5 +41,5 @@ export const usePageLayoutTabCreate = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
return { handleCreateTab };
|
||||
return { createPageLayoutTab };
|
||||
};
|
||||
+14
-10
@@ -1,8 +1,10 @@
|
||||
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 { type GraphSubType, type WidgetType } from '../mocks/mockWidgets';
|
||||
import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '../constants/SettingsPageLayoutTabsInstanceId';
|
||||
import { type GraphType, type WidgetType } from '../mocks/mockWidgets';
|
||||
import { pageLayoutCurrentLayoutsState } from '../states/pageLayoutCurrentLayoutsState';
|
||||
import { pageLayoutCurrentTabIdForCreationState } from '../states/pageLayoutCurrentTabIdForCreation';
|
||||
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
import { pageLayoutDraggedAreaState } from '../states/pageLayoutDraggedAreaState';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
@@ -15,10 +17,15 @@ import {
|
||||
} from '../utils/getDefaultWidgetData';
|
||||
import { getDefaultWidgetPosition } from '../utils/getDefaultWidgetPosition';
|
||||
|
||||
export const usePageLayoutWidgetCreate = () => {
|
||||
const handleCreateWidget = useRecoilCallback(
|
||||
export const useCreatePageLayoutWidget = () => {
|
||||
const activeTabId = useRecoilComponentValue(
|
||||
activeTabIdComponentState,
|
||||
SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const createPageLayoutWidget = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(widgetType: WidgetType, graphType: GraphSubType) => {
|
||||
(widgetType: WidgetType, graphType: GraphType) => {
|
||||
const widgetData = getDefaultWidgetData(graphType);
|
||||
|
||||
const pageLayoutDraft = snapshot
|
||||
@@ -30,9 +37,6 @@ export const usePageLayoutWidgetCreate = () => {
|
||||
const pageLayoutDraggedArea = snapshot
|
||||
.getLoadable(pageLayoutDraggedAreaState)
|
||||
.getValue();
|
||||
const activeTabId = snapshot
|
||||
.getLoadable(pageLayoutCurrentTabIdForCreationState)
|
||||
.getValue();
|
||||
|
||||
if (!activeTabId) {
|
||||
return;
|
||||
@@ -95,8 +99,8 @@ export const usePageLayoutWidgetCreate = () => {
|
||||
|
||||
set(pageLayoutDraggedAreaState, null);
|
||||
},
|
||||
[],
|
||||
[activeTabId],
|
||||
);
|
||||
|
||||
return { handleCreateWidget };
|
||||
return { createPageLayoutWidget };
|
||||
};
|
||||
+5
-4
@@ -1,11 +1,12 @@
|
||||
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 usePageLayoutWidgetDelete = () => {
|
||||
const handleRemoveWidget = useRecoilCallback(
|
||||
export const useDeletePageLayoutWidget = () => {
|
||||
const deletePageLayoutWidget = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(widgetId: string) => {
|
||||
const pageLayoutDraft = snapshot
|
||||
@@ -20,7 +21,7 @@ export const usePageLayoutWidgetDelete = () => {
|
||||
);
|
||||
const tabId = tabWithWidget?.id;
|
||||
|
||||
if (tabId !== undefined) {
|
||||
if (isDefined(tabId)) {
|
||||
const updatedLayouts = removeWidgetLayoutFromTab(
|
||||
allTabLayouts,
|
||||
tabId,
|
||||
@@ -37,5 +38,5 @@ export const usePageLayoutWidgetDelete = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
return { handleRemoveWidget };
|
||||
return { deletePageLayoutWidget };
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
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 };
|
||||
};
|
||||
-1
@@ -12,7 +12,6 @@ export const usePageLayoutDraftState = () => {
|
||||
? !isDeeplyEqual(pageLayoutDraft, {
|
||||
name: pageLayoutPersisted.name,
|
||||
type: pageLayoutPersisted.type,
|
||||
workspaceId: pageLayoutPersisted.workspaceId,
|
||||
objectMetadataId: pageLayoutPersisted.objectMetadataId,
|
||||
tabs: pageLayoutPersisted.tabs,
|
||||
})
|
||||
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { IconAppWindow } from 'twenty-ui/display';
|
||||
import { pageLayoutDraggedAreaState } from '../states/pageLayoutDraggedAreaState';
|
||||
import { pageLayoutSelectedCellsState } from '../states/pageLayoutSelectedCellsState';
|
||||
import { calculateGridBoundsFromSelectedCells } from '../utils/calculateGridBoundsFromSelectedCells';
|
||||
|
||||
export const usePageLayoutDragSelection = () => {
|
||||
const [pageLayoutSelectedCells, setPageLayoutSelectedCells] = useRecoilState(
|
||||
pageLayoutSelectedCellsState,
|
||||
);
|
||||
const setPageLayoutDraggedArea = useSetRecoilState(
|
||||
pageLayoutDraggedAreaState,
|
||||
);
|
||||
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
const handleDragSelectionStart = () => {
|
||||
setPageLayoutSelectedCells(new Set());
|
||||
};
|
||||
|
||||
const handleDragSelectionChange = (cellId: string, selected: boolean) => {
|
||||
setPageLayoutSelectedCells((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (selected) {
|
||||
newSet.add(cellId);
|
||||
} else {
|
||||
newSet.delete(cellId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragSelectionEnd = () => {
|
||||
if (pageLayoutSelectedCells.size > 0) {
|
||||
const draggedBounds = calculateGridBoundsFromSelectedCells(
|
||||
Array.from(pageLayoutSelectedCells),
|
||||
);
|
||||
|
||||
if (draggedBounds !== null) {
|
||||
setPageLayoutDraggedArea(draggedBounds);
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.PageLayoutWidgetTypeSelect,
|
||||
pageTitle: 'Add Widget',
|
||||
pageIcon: IconAppWindow,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
|
||||
setPageLayoutSelectedCells(new Set());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
pageLayoutSelectedCells,
|
||||
handleDragSelectionStart,
|
||||
handleDragSelectionChange,
|
||||
handleDragSelectionEnd,
|
||||
};
|
||||
};
|
||||
-1
@@ -42,7 +42,6 @@ export const usePageLayoutSaveHandler = () => {
|
||||
id: isEditMode ? id : uuidv4(),
|
||||
name: pageLayoutDraft.name,
|
||||
type: pageLayoutDraft.type,
|
||||
workspaceId: pageLayoutDraft.workspaceId,
|
||||
objectMetadataId: pageLayoutDraft.objectMetadataId,
|
||||
tabs: updatedTabs,
|
||||
createdAt: isEditMode
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { pageLayoutSelectedCellsState } from '../states/pageLayoutSelectedCellsState';
|
||||
|
||||
export const useStartPageLayoutDragSelection = () => {
|
||||
const startPageLayoutDragSelection = useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
set(pageLayoutSelectedCellsState, new Set());
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { startPageLayoutDragSelection };
|
||||
};
|
||||
+3
-3
@@ -2,8 +2,8 @@ import { useRecoilCallback } from 'recoil';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
|
||||
export const usePageLayoutWidgetUpdate = () => {
|
||||
const handleUpdateWidget = useRecoilCallback(
|
||||
export const useUpdatePageLayoutWidget = () => {
|
||||
const updatePageLayoutWidget = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(widgetId: string, updates: Partial<PageLayoutWidget>) => {
|
||||
set(pageLayoutDraftState, (prev) => ({
|
||||
@@ -19,5 +19,5 @@ export const usePageLayoutWidgetUpdate = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
return { handleUpdateWidget };
|
||||
return { updatePageLayoutWidget };
|
||||
};
|
||||
@@ -8,7 +8,7 @@ export enum WidgetType {
|
||||
GRAPH = 'GRAPH',
|
||||
}
|
||||
|
||||
export enum GraphSubType {
|
||||
export enum GraphType {
|
||||
NUMBER = 'NUMBER',
|
||||
GAUGE = 'GAUGE',
|
||||
PIE = 'PIE',
|
||||
@@ -29,7 +29,7 @@ export const mockPageLayoutWidgets: PageLayoutWidget[] = [
|
||||
columnSpan: 3,
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphSubType.NUMBER,
|
||||
graphType: GraphType.NUMBER,
|
||||
},
|
||||
data: {
|
||||
value: '1,234',
|
||||
@@ -52,7 +52,7 @@ export const mockPageLayoutWidgets: PageLayoutWidget[] = [
|
||||
columnSpan: 3,
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphSubType.GAUGE,
|
||||
graphType: GraphType.GAUGE,
|
||||
},
|
||||
data: {
|
||||
value: 0.5,
|
||||
@@ -77,7 +77,7 @@ export const mockPageLayoutWidgets: PageLayoutWidget[] = [
|
||||
columnSpan: 6,
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphSubType.PIE,
|
||||
graphType: GraphType.PIE,
|
||||
},
|
||||
data: {
|
||||
items: [
|
||||
@@ -130,7 +130,7 @@ export const mockPageLayoutWidgets: PageLayoutWidget[] = [
|
||||
columnSpan: 4,
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphSubType.BAR,
|
||||
graphType: GraphType.BAR,
|
||||
},
|
||||
data: {
|
||||
items: [
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const pageLayoutCurrentTabIdForCreationState = createState<
|
||||
string | null
|
||||
>({
|
||||
key: 'pageLayoutCurrentTabIdForCreationState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@@ -11,7 +11,6 @@ export const pageLayoutDraftState = createState<DraftPageLayout>({
|
||||
defaultValue: {
|
||||
name: '',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
workspaceId: undefined,
|
||||
objectMetadataId: null,
|
||||
tabs: [],
|
||||
},
|
||||
|
||||
@@ -43,7 +43,6 @@ export type SavedPageLayout = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: PageLayoutType;
|
||||
workspaceId?: string;
|
||||
objectMetadataId?: string | null;
|
||||
tabs: PageLayoutTab[];
|
||||
createdAt: string;
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { GraphSubType, WidgetType } from '../../mocks/mockWidgets';
|
||||
import { GraphType, WidgetType } from '../../mocks/mockWidgets';
|
||||
import { type PageLayoutWidget } from '../../states/savedPageLayoutsState';
|
||||
import { convertLayoutsToWidgets } from '../convertLayoutsToWidgets';
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('convertLayoutsToWidgets', () => {
|
||||
columnSpan: 2,
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphSubType.NUMBER,
|
||||
graphType: GraphType.NUMBER,
|
||||
},
|
||||
data: { value: 100 },
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
@@ -37,7 +37,7 @@ describe('convertLayoutsToWidgets', () => {
|
||||
columnSpan: 2,
|
||||
},
|
||||
configuration: {
|
||||
graphType: GraphSubType.PIE,
|
||||
graphType: GraphType.PIE,
|
||||
},
|
||||
data: { items: [] },
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
|
||||
+17
-20
@@ -1,14 +1,14 @@
|
||||
import { GraphSubType } from '../mocks/mockWidgets';
|
||||
import { GraphType } from '../mocks/mockWidgets';
|
||||
|
||||
export const getDefaultWidgetData = (graphType: GraphSubType) => {
|
||||
export const getDefaultWidgetData = (graphType: GraphType) => {
|
||||
switch (graphType) {
|
||||
case GraphSubType.NUMBER:
|
||||
case GraphType.NUMBER:
|
||||
return {
|
||||
value: '1,234',
|
||||
trendPercentage: 15.2,
|
||||
};
|
||||
|
||||
case GraphSubType.GAUGE:
|
||||
case GraphType.GAUGE:
|
||||
return {
|
||||
value: 0.7,
|
||||
min: 0,
|
||||
@@ -16,7 +16,7 @@ export const getDefaultWidgetData = (graphType: GraphSubType) => {
|
||||
label: 'Progress',
|
||||
};
|
||||
|
||||
case GraphSubType.PIE:
|
||||
case GraphType.PIE:
|
||||
return {
|
||||
items: [
|
||||
{ id: 'segment1', value: 35, label: 'Segment A' },
|
||||
@@ -26,7 +26,7 @@ export const getDefaultWidgetData = (graphType: GraphSubType) => {
|
||||
],
|
||||
};
|
||||
|
||||
case GraphSubType.BAR:
|
||||
case GraphType.BAR:
|
||||
return {
|
||||
items: [
|
||||
{ category: 'Jan', value: 45 },
|
||||
@@ -46,29 +46,26 @@ export const getDefaultWidgetData = (graphType: GraphSubType) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getWidgetTitle = (
|
||||
graphType: GraphSubType,
|
||||
index: number,
|
||||
): string => {
|
||||
const baseNames: Record<GraphSubType, string> = {
|
||||
[GraphSubType.NUMBER]: 'Number',
|
||||
[GraphSubType.GAUGE]: 'Gauge',
|
||||
[GraphSubType.PIE]: 'Pie Chart',
|
||||
[GraphSubType.BAR]: 'Bar Chart',
|
||||
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',
|
||||
};
|
||||
|
||||
return `${baseNames[graphType] || 'Widget'} ${index + 1}`;
|
||||
};
|
||||
|
||||
export const getWidgetSize = (graphType: GraphSubType) => {
|
||||
export const getWidgetSize = (graphType: GraphType) => {
|
||||
switch (graphType) {
|
||||
case GraphSubType.NUMBER:
|
||||
case GraphType.NUMBER:
|
||||
return { w: 3, h: 2 };
|
||||
case GraphSubType.GAUGE:
|
||||
case GraphType.GAUGE:
|
||||
return { w: 3, h: 3 };
|
||||
case GraphSubType.PIE:
|
||||
case GraphType.PIE:
|
||||
return { w: 4, h: 4 };
|
||||
case GraphSubType.BAR:
|
||||
case GraphType.BAR:
|
||||
return { w: 6, h: 4 };
|
||||
default:
|
||||
return { w: 4, h: 4 };
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { GraphWidgetBarChart } from '@/dashboards/widgets/graph/components/GraphWidgetBarChart';
|
||||
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 ReactNode } from 'react';
|
||||
import { GraphSubType } from '../mocks/mockWidgets';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
|
||||
type GraphRenderer = (widget: PageLayoutWidget) => ReactNode;
|
||||
|
||||
const graphRenderers: Record<GraphSubType, GraphRenderer> = {
|
||||
[GraphSubType.NUMBER]: (widget) => (
|
||||
<GraphWidgetNumberChart
|
||||
value={widget.data.value}
|
||||
trendPercentage={widget.data.trendPercentage}
|
||||
/>
|
||||
),
|
||||
[GraphSubType.GAUGE]: (widget) => (
|
||||
<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}`}
|
||||
/>
|
||||
),
|
||||
[GraphSubType.PIE]: (widget) => (
|
||||
<GraphWidgetPieChart
|
||||
data={widget.data.items}
|
||||
showLegend
|
||||
displayType="percentage"
|
||||
id={`pie-chart-${widget.id}`}
|
||||
/>
|
||||
),
|
||||
[GraphSubType.BAR]: (widget) => (
|
||||
<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}`}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const renderGraphWidget = (widget: PageLayoutWidget): ReactNode => {
|
||||
const graphType = widget.configuration?.graphType;
|
||||
|
||||
if (!graphType || typeof graphType !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Object.values(GraphSubType).includes(graphType as GraphSubType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderer = graphRenderers[graphType as GraphSubType];
|
||||
if (!renderer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return renderer(widget);
|
||||
};
|
||||
+72
@@ -1,5 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
IconCalendar,
|
||||
IconCheckbox,
|
||||
@@ -11,6 +12,8 @@ import {
|
||||
} from 'twenty-ui/display';
|
||||
import { ComponentWithRouterDecorator } from 'twenty-ui/testing';
|
||||
import { TabList } from '../TabList';
|
||||
import { type TabListProps } from '../../types/TabListProps';
|
||||
import { type SingleTabProps } from '../../types/SingleTabProps';
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general', title: 'General', logo: 'https://picsum.photos/200' },
|
||||
@@ -80,3 +83,72 @@ export const Default: Story = {
|
||||
</StyledInteractiveContainer>
|
||||
),
|
||||
};
|
||||
|
||||
type TabListWithAddProps = Pick<
|
||||
TabListProps,
|
||||
'componentInstanceId' | 'loading' | 'isInRightDrawer' | 'className'
|
||||
> & {
|
||||
initialTabs: SingleTabProps[];
|
||||
};
|
||||
|
||||
const TabListWithAdd = ({
|
||||
componentInstanceId,
|
||||
loading,
|
||||
isInRightDrawer,
|
||||
className,
|
||||
initialTabs,
|
||||
}: TabListWithAddProps) => {
|
||||
const [currentTabs, setCurrentTabs] = useState<SingleTabProps[]>(initialTabs);
|
||||
const [nextTabId, setNextTabId] = useState(initialTabs.length + 1);
|
||||
|
||||
const handleAddTab = () => {
|
||||
const newTab: SingleTabProps = {
|
||||
id: `new-tab-${nextTabId}`,
|
||||
title: `New Tab ${nextTabId}`,
|
||||
Icon: IconCheckbox,
|
||||
};
|
||||
setCurrentTabs([...currentTabs, newTab]);
|
||||
setNextTabId(nextTabId + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledInteractiveContainer>
|
||||
<p>
|
||||
<strong>Click the + button to add new tabs!</strong>
|
||||
</p>
|
||||
<TabList
|
||||
tabs={currentTabs}
|
||||
componentInstanceId={componentInstanceId}
|
||||
loading={loading}
|
||||
behaveAsLinks={false}
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
className={className}
|
||||
onAddTab={handleAddTab}
|
||||
/>
|
||||
</StyledInteractiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithAddTab: Story = {
|
||||
args: {
|
||||
componentInstanceId: 'tabs-with-add',
|
||||
tabs: tabs.slice(0, 3),
|
||||
},
|
||||
render: (args) => (
|
||||
<TabListWithAdd
|
||||
componentInstanceId={args.componentInstanceId}
|
||||
loading={args.loading}
|
||||
isInRightDrawer={args.isInRightDrawer}
|
||||
className={args.className}
|
||||
initialTabs={args.tabs}
|
||||
/>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'TabList with the ability to add new tabs dynamically using the onAddTab callback. Click the + button to add new tabs.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+2
-3
@@ -2,6 +2,7 @@ import { TAB_LIST_GAP } from '@/ui/layout/tab-list/constants/TabListGap';
|
||||
import { TAB_LIST_LEFT_PADDING } from '@/ui/layout/tab-list/constants/TabListPadding';
|
||||
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||
import { type TabWidthsById } from '@/ui/layout/tab-list/types/TabWidthsById';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type CalculateVisibleTabCountParams = {
|
||||
visibleTabs: SingleTabProps[];
|
||||
@@ -22,7 +23,6 @@ export const calculateVisibleTabCount = ({
|
||||
return visibleTabs.length;
|
||||
}
|
||||
|
||||
// Subtract add button width if present
|
||||
const availableWidth =
|
||||
containerWidth -
|
||||
TAB_LIST_LEFT_PADDING -
|
||||
@@ -33,8 +33,7 @@ export const calculateVisibleTabCount = ({
|
||||
const tab = visibleTabs[i];
|
||||
const tabWidth = tabWidthsById[tab.id];
|
||||
|
||||
// Skip if width not measured yet
|
||||
if (tabWidth === undefined) {
|
||||
if (!isDefined(tabWidth)) {
|
||||
return visibleTabs.length;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,27 +4,29 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons
|
||||
import { SettingsPageFullWidthContainer } from '@/settings/components/SettingsPageFullWidthContainer';
|
||||
import { PageLayoutInitializationEffect } from '@/settings/page-layout/components/PageLayoutInitializationEffect';
|
||||
import { PageLayoutWidgetPlaceholder } from '@/settings/page-layout/components/PageLayoutWidgetPlaceholder';
|
||||
import { WidgetRenderer } from '@/settings/page-layout/components/WidgetRenderer';
|
||||
import { EMPTY_LAYOUT } from '@/settings/page-layout/constants/EmptyLayout';
|
||||
import {
|
||||
PAGE_LAYOUT_CONFIG,
|
||||
type PageLayoutBreakpoint,
|
||||
} from '@/settings/page-layout/constants/PageLayoutBreakpoints';
|
||||
import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '@/settings/page-layout/constants/SettingsPageLayoutTabsInstanceId';
|
||||
import { useChangePageLayoutDragSelection } from '@/settings/page-layout/hooks/useChangePageLayoutDragSelection';
|
||||
import { useCreatePageLayoutTab } from '@/settings/page-layout/hooks/useCreatePageLayoutTab';
|
||||
import { useDeletePageLayoutWidget } from '@/settings/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { useEndPageLayoutDragSelection } from '@/settings/page-layout/hooks/useEndPageLayoutDragSelection';
|
||||
import { usePageLayoutDraftState } from '@/settings/page-layout/hooks/usePageLayoutDraftState';
|
||||
import { usePageLayoutDragSelection } from '@/settings/page-layout/hooks/usePageLayoutDragSelection';
|
||||
import { usePageLayoutHandleLayoutChange } from '@/settings/page-layout/hooks/usePageLayoutHandleLayoutChange';
|
||||
import { usePageLayoutSaveHandler } from '@/settings/page-layout/hooks/usePageLayoutSaveHandler';
|
||||
import { usePageLayoutTabCreate } from '@/settings/page-layout/hooks/usePageLayoutTabCreate';
|
||||
import { usePageLayoutWidgetDelete } from '@/settings/page-layout/hooks/usePageLayoutWidgetDelete';
|
||||
import { useStartPageLayoutDragSelection } from '@/settings/page-layout/hooks/useStartPageLayoutDragSelection';
|
||||
import { WidgetType } from '@/settings/page-layout/mocks/mockWidgets';
|
||||
import { pageLayoutCurrentBreakpointState } from '@/settings/page-layout/states/pageLayoutCurrentBreakpointState';
|
||||
import { pageLayoutCurrentLayoutsState } from '@/settings/page-layout/states/pageLayoutCurrentLayoutsState';
|
||||
import { pageLayoutCurrentTabIdForCreationState } from '@/settings/page-layout/states/pageLayoutCurrentTabIdForCreation';
|
||||
import { pageLayoutEditingWidgetIdState } from '@/settings/page-layout/states/pageLayoutEditingWidgetIdState';
|
||||
import { pageLayoutSelectedCellsState } from '@/settings/page-layout/states/pageLayoutSelectedCellsState';
|
||||
import { type PageLayoutWidget } from '@/settings/page-layout/states/savedPageLayoutsState';
|
||||
import { calculateTotalGridRows } from '@/settings/page-layout/utils/calculateTotalGridRows';
|
||||
import { generateCellId } from '@/settings/page-layout/utils/generateCellId';
|
||||
import { renderWidget } from '@/settings/page-layout/utils/widgetRegistry';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
@@ -143,9 +145,6 @@ export const SettingsPageLayoutEdit = () => {
|
||||
const pageLayoutCurrentLayouts = useRecoilValue(
|
||||
pageLayoutCurrentLayoutsState,
|
||||
);
|
||||
const setPageLayoutCurrentTabIdForCreation = useSetRecoilState(
|
||||
pageLayoutCurrentTabIdForCreationState,
|
||||
);
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
const setPageLayoutEditingWidgetId = useSetRecoilState(
|
||||
pageLayoutEditingWidgetIdState,
|
||||
@@ -155,7 +154,13 @@ export const SettingsPageLayoutEdit = () => {
|
||||
|
||||
const activeTabId = useRecoilComponentValue(
|
||||
activeTabIdComponentState,
|
||||
'page-layout-tabs',
|
||||
SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const setActiveTabId = useSetRecoilState(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID,
|
||||
}),
|
||||
);
|
||||
|
||||
const activeTabWidgets = useMemo(() => {
|
||||
@@ -171,25 +176,22 @@ export const SettingsPageLayoutEdit = () => {
|
||||
[pageLayoutDraft.tabs],
|
||||
);
|
||||
|
||||
const {
|
||||
handleDragSelectionStart,
|
||||
handleDragSelectionChange,
|
||||
handleDragSelectionEnd,
|
||||
} = usePageLayoutDragSelection();
|
||||
const { startPageLayoutDragSelection } = useStartPageLayoutDragSelection();
|
||||
const { changePageLayoutDragSelection } = useChangePageLayoutDragSelection();
|
||||
const { endPageLayoutDragSelection } = useEndPageLayoutDragSelection();
|
||||
|
||||
const handleOpenAddWidget = useCallback(() => {
|
||||
setPageLayoutCurrentTabIdForCreation(activeTabId);
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.PageLayoutWidgetTypeSelect,
|
||||
pageTitle: 'Add Widget',
|
||||
pageIcon: IconAppWindow,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
}, [navigateCommandMenu, activeTabId, setPageLayoutCurrentTabIdForCreation]);
|
||||
}, [navigateCommandMenu]);
|
||||
|
||||
const { handleRemoveWidget } = usePageLayoutWidgetDelete();
|
||||
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
|
||||
const { handleLayoutChange } = usePageLayoutHandleLayoutChange(activeTabId);
|
||||
const { handleCreateTab } = usePageLayoutTabCreate();
|
||||
const { createPageLayoutTab } = useCreatePageLayoutTab();
|
||||
|
||||
const handleEditWidget = useCallback(
|
||||
(widgetId: string) => {
|
||||
@@ -197,9 +199,6 @@ export const SettingsPageLayoutEdit = () => {
|
||||
if (!widget) return;
|
||||
|
||||
setPageLayoutEditingWidgetId(widgetId);
|
||||
setPageLayoutCurrentTabIdForCreation(
|
||||
widget.pageLayoutTabId || activeTabId,
|
||||
);
|
||||
|
||||
if (widget.type === WidgetType.IFRAME) {
|
||||
navigateCommandMenu({
|
||||
@@ -210,13 +209,7 @@ export const SettingsPageLayoutEdit = () => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
allWidgets,
|
||||
setPageLayoutEditingWidgetId,
|
||||
navigateCommandMenu,
|
||||
setPageLayoutCurrentTabIdForCreation,
|
||||
activeTabId,
|
||||
],
|
||||
[allWidgets, setPageLayoutEditingWidgetId, navigateCommandMenu],
|
||||
);
|
||||
|
||||
const isEmptyState = activeTabWidgets.length === 0;
|
||||
@@ -233,16 +226,10 @@ export const SettingsPageLayoutEdit = () => {
|
||||
navigateSettings(SettingsPath.PageLayout);
|
||||
};
|
||||
|
||||
const setActiveTabId = useSetRecoilState(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: 'page-layout-tabs',
|
||||
}),
|
||||
);
|
||||
|
||||
const handleAddTab = useCallback(() => {
|
||||
const newTabId = handleCreateTab();
|
||||
const newTabId = createPageLayoutTab();
|
||||
setActiveTabId(newTabId);
|
||||
}, [handleCreateTab, setActiveTabId]);
|
||||
}, [createPageLayoutTab, setActiveTabId]);
|
||||
|
||||
const tabListTabs: SingleTabProps[] = useMemo(() => {
|
||||
return [...pageLayoutDraft.tabs]
|
||||
@@ -334,7 +321,7 @@ export const SettingsPageLayoutEdit = () => {
|
||||
<StyledTabList
|
||||
tabs={tabListTabs}
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId="page-layout-tabs"
|
||||
componentInstanceId={SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID}
|
||||
onAddTab={handleAddTab}
|
||||
/>
|
||||
)}
|
||||
@@ -394,10 +381,10 @@ export const SettingsPageLayoutEdit = () => {
|
||||
<div key={widget.id} data-select-disable="true">
|
||||
<PageLayoutWidgetPlaceholder
|
||||
title={widget.title}
|
||||
onRemove={() => handleRemoveWidget(widget.id)}
|
||||
onRemove={() => deletePageLayoutWidget(widget.id)}
|
||||
onEdit={() => handleEditWidget(widget.id)}
|
||||
>
|
||||
{renderWidget(widget)}
|
||||
<WidgetRenderer widget={widget} />
|
||||
</PageLayoutWidgetPlaceholder>
|
||||
</div>
|
||||
))
|
||||
@@ -406,9 +393,9 @@ export const SettingsPageLayoutEdit = () => {
|
||||
{pageLayoutCurrentBreakpoint !== 'mobile' && (
|
||||
<DragSelect
|
||||
selectableItemsContainerRef={gridContainerRef}
|
||||
onDragSelectionStart={handleDragSelectionStart}
|
||||
onDragSelectionChange={handleDragSelectionChange}
|
||||
onDragSelectionEnd={handleDragSelectionEnd}
|
||||
onDragSelectionStart={startPageLayoutDragSelection}
|
||||
onDragSelectionChange={changePageLayoutDragSelection}
|
||||
onDragSelectionEnd={endPageLayoutDragSelection}
|
||||
/>
|
||||
)}
|
||||
</StyledGridContainer>
|
||||
|
||||
Reference in New Issue
Block a user