Add position to page layout widgets (#17643)
## Context
Introducing a new position column in PageLayoutWidget. This is because
we already have gridPosition but all widgets are not supposed to fit in
a grid (see pageLayoutTab layoutMode (GRID, VERTICAL_LIST, CANVAS...),
position will now support the different layoutMode
## Implementation
- Adding the new column (not renaming to avoid breaking changes in the
dashboards)
- Add proper validation for each layout mode
- Update standard page layout to not always use GRID but rely on the tab
layout mode
<details>
<summary>Graphql Query</summary>
```graphql
{
"data": {
"getPageLayouts": [
{
"tabs": [
{
"title": "Tab 1",
"layoutMode": "GRID",
"widgets": [
{
"title": "Untitled Rich Text",
"position": {
"column": 0,
"columnSpan": 6,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 6
}
},
{
"title": "Deals by Company",
"position": {
"column": 6,
"columnSpan": 6,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 6
}
},
{
"title": "Pipeline Value by Stage",
"position": {
"column": 0,
"columnSpan": 6,
"layoutMode": "GRID",
"row": 6,
"rowSpan": 6
}
},
{
"title": "Revenue Timeline",
"position": {
"column": 6,
"columnSpan": 6,
"layoutMode": "GRID",
"row": 6,
"rowSpan": 6
}
},
{
"title": "Opportunities by Owner",
"position": {
"column": 0,
"columnSpan": 6,
"layoutMode": "GRID",
"row": 12,
"rowSpan": 6
}
},
{
"title": "Stock market (Iframe)",
"position": {
"column": 6,
"columnSpan": 6,
"layoutMode": "GRID",
"row": 12,
"rowSpan": 8
}
},
{
"title": "Deals created this month",
"position": {
"column": 0,
"columnSpan": 3,
"layoutMode": "GRID",
"row": 18,
"rowSpan": 2
}
},
{
"title": "Deal value created this month",
"position": {
"column": 3,
"columnSpan": 3,
"layoutMode": "GRID",
"row": 18,
"rowSpan": 2
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
}
]
},
{
"title": "Timeline",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Timeline",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Tasks",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Tasks",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Notes",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Notes",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Files",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Files",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Emails",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Emails",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Calendar",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Calendar",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
}
]
},
{
"title": "Timeline",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Timeline",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Tasks",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Tasks",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Notes",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Notes",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Files",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Files",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Emails",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Emails",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Calendar",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Calendar",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
}
]
},
{
"title": "Timeline",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Timeline",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Tasks",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Tasks",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Notes",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Notes",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Files",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Files",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Emails",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Emails",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Calendar",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Calendar",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
},
{
"title": "Note",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 1
}
}
]
},
{
"title": "Note",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Note",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Timeline",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Timeline",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Files",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Files",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
},
{
"title": "Note",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 1
}
}
]
},
{
"title": "Note",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Note",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Timeline",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Timeline",
"position": {
"layoutMode": "CANVAS"
}
}
]
},
{
"title": "Files",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Files",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Flow",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Flow",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
}
]
},
{
"title": "Flow",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Flow",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
},
{
"tabs": [
{
"title": "Home",
"layoutMode": "VERTICAL_LIST",
"widgets": [
{
"title": "Fields",
"position": {
"layoutMode": "VERTICAL_LIST",
"index": 0
}
}
]
},
{
"title": "Flow",
"layoutMode": "CANVAS",
"widgets": [
{
"title": "Flow",
"position": {
"layoutMode": "CANVAS"
}
}
]
}
]
}
]
}
}
```
</details>
This commit is contained in:
+74
@@ -0,0 +1,74 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type PageLayoutWidgetGridPosition } from 'twenty-shared/types';
|
||||
|
||||
import { WIDGET_GRID_MAX_COLUMNS } from 'src/engine/metadata-modules/page-layout-widget/constants/widget-grid-max-columns.constant';
|
||||
import { WIDGET_GRID_MAX_ROWS } from 'src/engine/metadata-modules/page-layout-widget/constants/widget-grid-max-rows.constant';
|
||||
import {
|
||||
PageLayoutWidgetExceptionCode,
|
||||
PageLayoutWidgetExceptionMessageKey,
|
||||
generatePageLayoutWidgetExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
|
||||
export const validatePageLayoutWidgetGridPosition = (
|
||||
position: PageLayoutWidgetGridPosition,
|
||||
widgetTitle: string,
|
||||
): FlatEntityValidationError[] => {
|
||||
const errors: FlatEntityValidationError[] = [];
|
||||
|
||||
const { row, column, rowSpan, columnSpan } = position;
|
||||
|
||||
if (column >= WIDGET_GRID_MAX_COLUMNS) {
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`column ${column} exceeds grid width (max column is ${WIDGET_GRID_MAX_COLUMNS - 1})`,
|
||||
),
|
||||
userFriendlyMessage: msg`Widget extends beyond grid width`,
|
||||
});
|
||||
}
|
||||
|
||||
if (column + columnSpan > WIDGET_GRID_MAX_COLUMNS) {
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`widget extends beyond grid width (column ${column} + columnSpan ${columnSpan} > ${WIDGET_GRID_MAX_COLUMNS})`,
|
||||
),
|
||||
userFriendlyMessage: msg`Widget extends beyond grid width`,
|
||||
});
|
||||
}
|
||||
|
||||
if (row >= WIDGET_GRID_MAX_ROWS) {
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`row ${row} exceeds maximum allowed rows (${WIDGET_GRID_MAX_ROWS})`,
|
||||
),
|
||||
userFriendlyMessage: msg`Widget row exceeds grid height`,
|
||||
});
|
||||
}
|
||||
|
||||
if (row + rowSpan > WIDGET_GRID_MAX_ROWS) {
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`widget extends beyond grid height (row ${row} + rowSpan ${rowSpan} > ${WIDGET_GRID_MAX_ROWS})`,
|
||||
),
|
||||
userFriendlyMessage: msg`Widget extends beyond grid height`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type PageLayoutWidgetVerticalListPosition } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
PageLayoutWidgetExceptionCode,
|
||||
PageLayoutWidgetExceptionMessageKey,
|
||||
generatePageLayoutWidgetExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
|
||||
export const validatePageLayoutWidgetVerticalListPosition = (
|
||||
position: PageLayoutWidgetVerticalListPosition,
|
||||
widgetTitle: string,
|
||||
): FlatEntityValidationError[] => {
|
||||
const errors: FlatEntityValidationError[] = [];
|
||||
|
||||
if (position.index < 0) {
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`index ${position.index} must be a non-negative integer`,
|
||||
),
|
||||
userFriendlyMessage: msg`Widget index must be a non-negative integer`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!Number.isInteger(position.index)) {
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`index ${position.index} must be an integer`,
|
||||
),
|
||||
userFriendlyMessage: msg`Widget index must be an integer`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
+5
-4
@@ -16,6 +16,7 @@ type GridPosition = {
|
||||
columnSpan: number;
|
||||
};
|
||||
|
||||
// TODO: remove in favor of validatePageLayoutWidgetGridPosition once gridPosition is deprecated
|
||||
export const validateWidgetGridPosition = (
|
||||
gridPosition: GridPosition,
|
||||
widgetTitle: string,
|
||||
@@ -28,7 +29,7 @@ export const validateWidgetGridPosition = (
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION,
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`column ${column} exceeds grid width (max column is ${WIDGET_GRID_MAX_COLUMNS - 1})`,
|
||||
@@ -41,7 +42,7 @@ export const validateWidgetGridPosition = (
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION,
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`widget extends beyond grid width (column ${column} + columnSpan ${columnSpan} > ${WIDGET_GRID_MAX_COLUMNS})`,
|
||||
@@ -54,7 +55,7 @@ export const validateWidgetGridPosition = (
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION,
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`row ${row} exceeds maximum allowed rows (${WIDGET_GRID_MAX_ROWS})`,
|
||||
@@ -67,7 +68,7 @@ export const validateWidgetGridPosition = (
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION,
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION,
|
||||
widgetTitle,
|
||||
undefined,
|
||||
`widget extends beyond grid height (row ${row} + rowSpan ${rowSpan} > ${WIDGET_GRID_MAX_ROWS})`,
|
||||
|
||||
Reference in New Issue
Block a user