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:
@@ -0,0 +1,89 @@
|
||||
import { WidgetType } from '../../mocks/mockWidgets';
|
||||
import {
|
||||
type PageLayoutTabWithData,
|
||||
type PageLayoutWidgetWithData,
|
||||
} from '../../types/pageLayoutTypes';
|
||||
import { addWidgetToTab } from '../addWidgetToTab';
|
||||
|
||||
describe('addWidgetToTab', () => {
|
||||
const mockWidget: PageLayoutWidgetWithData = {
|
||||
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: PageLayoutTabWithData[] = [
|
||||
{
|
||||
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: PageLayoutWidgetWithData = {
|
||||
...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
@@ -0,0 +1,138 @@
|
||||
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
@@ -0,0 +1,40 @@
|
||||
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
@@ -0,0 +1,102 @@
|
||||
import { GraphType, WidgetType } from '../../mocks/mockWidgets';
|
||||
import { type PageLayoutWidgetWithData } from '../../types/pageLayoutTypes';
|
||||
import { convertLayoutsToWidgets } from '../convertLayoutsToWidgets';
|
||||
|
||||
describe('convertLayoutsToWidgets', () => {
|
||||
const mockWidgets: PageLayoutWidgetWithData[] = [
|
||||
{
|
||||
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
@@ -0,0 +1,79 @@
|
||||
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
@@ -0,0 +1,82 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
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
@@ -0,0 +1,23 @@
|
||||
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
@@ -0,0 +1,18 @@
|
||||
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
@@ -0,0 +1,109 @@
|
||||
import { WidgetType } from '../../mocks/mockWidgets';
|
||||
import { type PageLayoutTabWithData } from '../../types/pageLayoutTypes';
|
||||
import { removeWidgetFromTab } from '../removeWidgetFromTab';
|
||||
|
||||
describe('removeWidgetFromTab', () => {
|
||||
const mockTabs: PageLayoutTabWithData[] = [
|
||||
{
|
||||
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
@@ -0,0 +1,126 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
type PageLayoutTabWithData,
|
||||
type PageLayoutWidgetWithData,
|
||||
} from '../types/pageLayoutTypes';
|
||||
|
||||
export const addWidgetToTab = (
|
||||
tabs: PageLayoutTabWithData[],
|
||||
activeTabId: string,
|
||||
newWidget: PageLayoutWidgetWithData,
|
||||
): PageLayoutTabWithData[] => {
|
||||
return tabs.map((tab) => {
|
||||
if (tab.id === activeTabId) {
|
||||
return {
|
||||
...tab,
|
||||
widgets: [...tab.widgets, newWidget],
|
||||
};
|
||||
}
|
||||
return tab;
|
||||
});
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
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);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { type PageLayoutWidgetWithData } from '@/page-layout/types/pageLayoutTypes';
|
||||
import { type Layouts } from 'react-grid-layout';
|
||||
|
||||
export const convertLayoutsToWidgets = (
|
||||
widgets: PageLayoutWidgetWithData[],
|
||||
layouts: Layouts,
|
||||
): PageLayoutWidgetWithData[] => {
|
||||
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,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { type TabLayouts } from '../states/pageLayoutCurrentLayoutsState';
|
||||
|
||||
export const createEmptyTabLayout = (
|
||||
allTabLayouts: TabLayouts,
|
||||
tabId: string,
|
||||
): TabLayouts => {
|
||||
return {
|
||||
...allTabLayouts,
|
||||
[tabId]: { desktop: [], mobile: [] },
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
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 }],
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const generateCellId = (col: number, row: number): string => {
|
||||
return `cell-${col}-${row}`;
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
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,
|
||||
xAxisLabel: 'Time',
|
||||
yAxisLabel: 'Value',
|
||||
curve: 'monotoneX',
|
||||
displayType: 'shortNumber',
|
||||
prefix: '$',
|
||||
suffix: '',
|
||||
xScale: { type: 'linear' },
|
||||
yScale: { type: 'linear', min: 0, max: 'auto' },
|
||||
stackedArea: false,
|
||||
enableSlices: false,
|
||||
};
|
||||
|
||||
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 };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type PageLayout } from '~/generated/graphql';
|
||||
import { type PageLayoutWithData } from '../types/pageLayoutTypes';
|
||||
|
||||
export const normalizePageLayoutData = (
|
||||
pageLayout: PageLayout,
|
||||
): PageLayoutWithData => {
|
||||
return {
|
||||
...pageLayout,
|
||||
tabs: (pageLayout.tabs || []).map((tab) => ({
|
||||
...tab,
|
||||
widgets: (tab.widgets || []).map((widget) => ({
|
||||
...widget,
|
||||
data: undefined,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
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 };
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type PageLayoutTabWithData } from '../types/pageLayoutTypes';
|
||||
|
||||
export const removeWidgetFromTab = (
|
||||
tabs: PageLayoutTabWithData[],
|
||||
tabId: string,
|
||||
widgetId: string,
|
||||
): PageLayoutTabWithData[] => {
|
||||
return tabs.map((tab) => {
|
||||
if (tab.id === tabId) {
|
||||
return {
|
||||
...tab,
|
||||
widgets: tab.widgets.filter((w) => w.id !== widgetId),
|
||||
};
|
||||
}
|
||||
return tab;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
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