Page layout refactoring (#14535)

In this PR:
- Refactored the page layout renderer
- Converted all the states to component states
- Removed the page layout edition in settings

TODOs in next PRs:
- Fix bug with the drag selector not taking the scroll into account to
display the dragged area
- Readd the tab edition in edit mode
This commit is contained in:
Raphaël Bosi
2025-09-16 17:42:51 +02:00
committed by GitHub
parent e69131790c
commit 3b868c3d2a
90 changed files with 2070 additions and 1928 deletions
@@ -1,7 +1,7 @@
import { type TabLayouts } from '../../states/pageLayoutCurrentLayoutsState';
import { createEmptyTabLayout } from '../createEmptyTabLayout';
import { type TabLayouts } from '@/page-layout/types/tab-layouts';
import { getEmptyTabLayout } from '../getEmptyTabLayout';
describe('createEmptyTabLayout', () => {
describe('getEmptyTabLayout', () => {
const mockTabLayouts: TabLayouts = {
'tab-1': {
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2 }],
@@ -14,7 +14,7 @@ describe('createEmptyTabLayout', () => {
};
it('should create empty layout for a new tab', () => {
const result = createEmptyTabLayout(mockTabLayouts, 'tab-3');
const result = getEmptyTabLayout(mockTabLayouts, 'tab-3');
expect(result['tab-3']).toBeDefined();
expect(result['tab-3'].desktop).toEqual([]);
@@ -22,7 +22,7 @@ describe('createEmptyTabLayout', () => {
});
it('should preserve existing tabs', () => {
const result = createEmptyTabLayout(mockTabLayouts, 'tab-3');
const result = getEmptyTabLayout(mockTabLayouts, 'tab-3');
expect(result['tab-1']).toEqual(mockTabLayouts['tab-1']);
expect(result['tab-2']).toEqual(mockTabLayouts['tab-2']);
@@ -30,7 +30,7 @@ describe('createEmptyTabLayout', () => {
});
it('should overwrite existing tab with empty layout', () => {
const result = createEmptyTabLayout(mockTabLayouts, 'tab-1');
const result = getEmptyTabLayout(mockTabLayouts, 'tab-1');
expect(result['tab-1'].desktop).toEqual([]);
expect(result['tab-1'].mobile).toEqual([]);
@@ -39,7 +39,7 @@ describe('createEmptyTabLayout', () => {
it('should work with empty initial state', () => {
const emptyLayouts: TabLayouts = {};
const result = createEmptyTabLayout(emptyLayouts, 'tab-1');
const result = getEmptyTabLayout(emptyLayouts, 'tab-1');
expect(result['tab-1']).toBeDefined();
expect(result['tab-1'].desktop).toEqual([]);
@@ -49,7 +49,7 @@ describe('createEmptyTabLayout', () => {
it('should return a new object without mutating the original', () => {
const originalLayouts = JSON.parse(JSON.stringify(mockTabLayouts));
const result = createEmptyTabLayout(mockTabLayouts, 'tab-3');
const result = getEmptyTabLayout(mockTabLayouts, 'tab-3');
expect(result).not.toBe(mockTabLayouts);
expect(mockTabLayouts).toEqual(originalLayouts);
@@ -58,9 +58,9 @@ describe('createEmptyTabLayout', () => {
});
it('should handle multiple new tabs', () => {
let result = createEmptyTabLayout(mockTabLayouts, 'tab-3');
result = createEmptyTabLayout(result, 'tab-4');
result = createEmptyTabLayout(result, 'tab-5');
let result = getEmptyTabLayout(mockTabLayouts, 'tab-3');
result = getEmptyTabLayout(result, 'tab-4');
result = getEmptyTabLayout(result, 'tab-5');
expect(Object.keys(result)).toHaveLength(5);
expect(result['tab-3'].desktop).toEqual([]);
@@ -69,7 +69,7 @@ describe('createEmptyTabLayout', () => {
});
it('should create consistent structure for desktop and mobile', () => {
const result = createEmptyTabLayout(mockTabLayouts, 'new-tab');
const result = getEmptyTabLayout(mockTabLayouts, 'new-tab');
expect(result['new-tab']).toHaveProperty('desktop');
expect(result['new-tab']).toHaveProperty('mobile');
@@ -1,7 +1,7 @@
import { type TabLayouts } from '../../states/pageLayoutCurrentLayoutsState';
import { createUpdatedTabLayouts } from '../createUpdatedTabLayouts';
import { type TabLayouts } from '@/page-layout/types/tab-layouts';
import { getUpdatedTabLayouts } from '../getUpdatedTabLayouts';
describe('createUpdatedTabLayouts', () => {
describe('getUpdatedTabLayouts', () => {
const mockTabLayouts: TabLayouts = {
'tab-1': {
desktop: [{ i: 'widget-1', x: 0, y: 0, w: 2, h: 2 }],
@@ -16,7 +16,7 @@ describe('createUpdatedTabLayouts', () => {
const newLayout = { i: 'widget-2', x: 2, y: 0, w: 3, h: 3 };
it('should add new layout to existing tab', () => {
const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout);
const result = getUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout);
expect(result['tab-1'].desktop).toHaveLength(2);
expect(result['tab-1'].desktop[1]).toEqual(newLayout);
@@ -25,7 +25,7 @@ describe('createUpdatedTabLayouts', () => {
});
it('should add layout to empty tab', () => {
const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-2', newLayout);
const result = getUpdatedTabLayouts(mockTabLayouts, 'tab-2', newLayout);
expect(result['tab-2'].desktop).toHaveLength(1);
expect(result['tab-2'].desktop[0]).toEqual(newLayout);
@@ -34,7 +34,7 @@ describe('createUpdatedTabLayouts', () => {
});
it('should create new tab entry if tab does not exist', () => {
const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-3', newLayout);
const result = getUpdatedTabLayouts(mockTabLayouts, 'tab-3', newLayout);
expect(result['tab-3']).toBeDefined();
expect(result['tab-3'].desktop).toHaveLength(1);
@@ -43,14 +43,14 @@ describe('createUpdatedTabLayouts', () => {
});
it('should not modify other tabs', () => {
const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout);
const result = getUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout);
expect(result['tab-2']).toEqual(mockTabLayouts['tab-2']);
});
it('should handle mobile layout transformation correctly', () => {
const wideLayout = { i: 'widget-3', x: 5, y: 2, w: 6, h: 4 };
const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-1', wideLayout);
const result = getUpdatedTabLayouts(mockTabLayouts, 'tab-1', wideLayout);
const mobileLayout =
result['tab-1'].mobile[result['tab-1'].mobile.length - 1];
@@ -62,7 +62,7 @@ describe('createUpdatedTabLayouts', () => {
it('should return a new object without mutating the original', () => {
const originalLayouts = JSON.parse(JSON.stringify(mockTabLayouts));
const result = createUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout);
const result = getUpdatedTabLayouts(mockTabLayouts, 'tab-1', newLayout);
expect(result).not.toBe(mockTabLayouts);
expect(mockTabLayouts).toEqual(originalLayouts);
@@ -76,7 +76,7 @@ describe('createUpdatedTabLayouts', () => {
};
expect(() => {
createUpdatedTabLayouts(malformedLayouts, 'tab-1', newLayout);
getUpdatedTabLayouts(malformedLayouts, 'tab-1', newLayout);
}).toThrow();
});
});
@@ -1,4 +1,4 @@
import { type TabLayouts } from '../../states/pageLayoutCurrentLayoutsState';
import { type TabLayouts } from '@/page-layout/types/tab-layouts';
import { removeWidgetLayoutFromTab } from '../removeWidgetLayoutFromTab';
describe('removeWidgetLayoutFromTab', () => {
@@ -0,0 +1,12 @@
export const calculateGridCellPosition = ({
index,
numberOfColumns,
}: {
index: number;
numberOfColumns: number;
}) => {
const column = index % numberOfColumns;
const row = Math.floor(index / numberOfColumns);
return { row, column };
};
@@ -9,6 +9,7 @@ export const convertLayoutsToWidgets = (
return widgets.map((widget) => {
const layout = activeLayouts.find((l) => l.i === widget.id);
return {
...widget,
gridPosition: {
@@ -1,6 +1,6 @@
import { type TabLayouts } from '../states/pageLayoutCurrentLayoutsState';
import { type TabLayouts } from '@/page-layout/types/tab-layouts';
export const createEmptyTabLayout = (
export const getEmptyTabLayout = (
allTabLayouts: TabLayouts,
tabId: string,
): TabLayouts => {
@@ -0,0 +1,3 @@
export const getTabListInstanceIdFromPageLayoutId = (pageLayoutId: string) => {
return `${pageLayoutId}-tab-list`;
};
@@ -1,6 +1,6 @@
import { type TabLayouts } from '../states/pageLayoutCurrentLayoutsState';
import { type TabLayouts } from '@/page-layout/types/tab-layouts';
export const createUpdatedTabLayouts = (
export const getUpdatedTabLayouts = (
allTabLayouts: TabLayouts,
activeTabId: string,
newLayout: { i: string; x: number; y: number; w: number; h: number },
@@ -1,17 +0,0 @@
import { type PageLayout } from '~/generated/graphql';
import { type PageLayoutWithData } from '../types/pageLayoutTypes';
export const normalizePageLayoutData = (
pageLayout: PageLayout,
): PageLayoutWithData => {
return {
...pageLayout,
tabs: (pageLayout.tabs || []).map((tab) => ({
...tab,
widgets: (tab.widgets || []).map((widget) => ({
...widget,
data: undefined,
})),
})),
};
};
@@ -1,4 +1,4 @@
import { type TabLayouts } from '../states/pageLayoutCurrentLayoutsState';
import { type TabLayouts } from '@/page-layout/types/tab-layouts';
export const removeWidgetLayoutFromTab = (
allTabLayouts: TabLayouts,