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:
+79
@@ -2,10 +2,15 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutWidgetPosition,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
|
||||
import { PageLayoutTabExceptionCode } from 'src/engine/metadata-modules/page-layout-tab/exceptions/page-layout-tab.exception';
|
||||
import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum';
|
||||
@@ -13,6 +18,8 @@ import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums
|
||||
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
|
||||
import { GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type';
|
||||
import { validatePageLayoutWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-grid-position.util';
|
||||
import { validatePageLayoutWidgetVerticalListPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-page-layout-widget-vertical-list-position.util';
|
||||
import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util';
|
||||
import {
|
||||
FailedFlatEntityValidation,
|
||||
@@ -80,6 +87,12 @@ export class FlatPageLayoutWidgetValidatorService {
|
||||
pageLayoutTabId: updatedFlatPageLayoutWidget.pageLayoutTabId,
|
||||
};
|
||||
|
||||
const referencedPageLayoutTab = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: updatedFlatPageLayoutWidget.pageLayoutTabId,
|
||||
flatEntityMaps:
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutTabMaps,
|
||||
});
|
||||
|
||||
const gridPositionErrors = this.validateGridPosition({
|
||||
gridPosition: updatedFlatPageLayoutWidget.gridPosition,
|
||||
widgetTitle: updatedFlatPageLayoutWidget.title,
|
||||
@@ -87,6 +100,14 @@ export class FlatPageLayoutWidgetValidatorService {
|
||||
|
||||
validationResult.errors.push(...gridPositionErrors);
|
||||
|
||||
const positionErrors = this.validatePosition({
|
||||
position: updatedFlatPageLayoutWidget.position,
|
||||
pageLayoutTab: referencedPageLayoutTab,
|
||||
widgetTitle: updatedFlatPageLayoutWidget.title,
|
||||
});
|
||||
|
||||
validationResult.errors.push(...positionErrors);
|
||||
|
||||
const featureFlagErrors = this.validateFeatureFlags({
|
||||
type: updatedFlatPageLayoutWidget.type,
|
||||
configuration: updatedFlatPageLayoutWidget.configuration,
|
||||
@@ -209,6 +230,14 @@ export class FlatPageLayoutWidgetValidatorService {
|
||||
|
||||
validationResult.errors.push(...gridPositionErrors);
|
||||
|
||||
const positionErrors = this.validatePosition({
|
||||
position: flatPageLayoutWidgetToValidate.position,
|
||||
pageLayoutTab: referencedPageLayoutTab,
|
||||
widgetTitle: flatPageLayoutWidgetToValidate.title,
|
||||
});
|
||||
|
||||
validationResult.errors.push(...positionErrors);
|
||||
|
||||
const featureFlagErrors = this.validateFeatureFlags({
|
||||
type: flatPageLayoutWidgetToValidate.type,
|
||||
configuration: flatPageLayoutWidgetToValidate.configuration,
|
||||
@@ -295,4 +324,54 @@ export class FlatPageLayoutWidgetValidatorService {
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private validatePosition({
|
||||
position,
|
||||
pageLayoutTab,
|
||||
widgetTitle,
|
||||
}: {
|
||||
position: PageLayoutWidgetPosition | null | undefined;
|
||||
pageLayoutTab: FlatPageLayoutTab | undefined;
|
||||
widgetTitle: string;
|
||||
}): FlatEntityValidationError[] {
|
||||
if (!isDefined(position)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const errors: FlatEntityValidationError[] = [];
|
||||
|
||||
if (
|
||||
isDefined(pageLayoutTab) &&
|
||||
position.layoutMode !== pageLayoutTab.layoutMode
|
||||
) {
|
||||
const layoutMode = position.layoutMode;
|
||||
const tabLayoutMode = pageLayoutTab.layoutMode;
|
||||
|
||||
errors.push({
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: t`Position layoutMode "${layoutMode}" does not match tab layoutMode "${tabLayoutMode}"`,
|
||||
userFriendlyMessage: msg`Widget position type must match the tab layout mode`,
|
||||
});
|
||||
}
|
||||
|
||||
switch (position.layoutMode) {
|
||||
case PageLayoutTabLayoutMode.GRID:
|
||||
errors.push(
|
||||
...validatePageLayoutWidgetGridPosition(position, widgetTitle),
|
||||
);
|
||||
break;
|
||||
case PageLayoutTabLayoutMode.VERTICAL_LIST:
|
||||
errors.push(
|
||||
...validatePageLayoutWidgetVerticalListPosition(
|
||||
position,
|
||||
widgetTitle,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case PageLayoutTabLayoutMode.CANVAS:
|
||||
break;
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user