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:
@@ -1047,6 +1047,7 @@ export type CreatePageLayoutWidgetInput = {
|
||||
gridPosition: GridPositionInput;
|
||||
objectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
pageLayoutTabId: Scalars['UUID'];
|
||||
position?: InputMaybe<Scalars['JSON']>;
|
||||
title: Scalars['String'];
|
||||
type: WidgetType;
|
||||
};
|
||||
@@ -3424,11 +3425,34 @@ export type PageLayoutWidget = {
|
||||
id: Scalars['UUID'];
|
||||
objectMetadataId?: Maybe<Scalars['UUID']>;
|
||||
pageLayoutTabId: Scalars['UUID'];
|
||||
position?: Maybe<PageLayoutWidgetPosition>;
|
||||
title: Scalars['String'];
|
||||
type: WidgetType;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type PageLayoutWidgetCanvasPosition = {
|
||||
__typename?: 'PageLayoutWidgetCanvasPosition';
|
||||
layoutMode: PageLayoutTabLayoutMode;
|
||||
};
|
||||
|
||||
export type PageLayoutWidgetGridPosition = {
|
||||
__typename?: 'PageLayoutWidgetGridPosition';
|
||||
column: Scalars['Int'];
|
||||
columnSpan: Scalars['Int'];
|
||||
layoutMode: PageLayoutTabLayoutMode;
|
||||
row: Scalars['Int'];
|
||||
rowSpan: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type PageLayoutWidgetPosition = PageLayoutWidgetCanvasPosition | PageLayoutWidgetGridPosition | PageLayoutWidgetVerticalListPosition;
|
||||
|
||||
export type PageLayoutWidgetVerticalListPosition = {
|
||||
__typename?: 'PageLayoutWidgetVerticalListPosition';
|
||||
index: Scalars['Int'];
|
||||
layoutMode: PageLayoutTabLayoutMode;
|
||||
};
|
||||
|
||||
export type PermissionFlag = {
|
||||
__typename?: 'PermissionFlag';
|
||||
flag: PermissionFlagType;
|
||||
@@ -4713,6 +4737,7 @@ export type UpdatePageLayoutWidgetInput = {
|
||||
configuration?: InputMaybe<Scalars['JSON']>;
|
||||
gridPosition?: InputMaybe<GridPositionInput>;
|
||||
objectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
position?: InputMaybe<Scalars['JSON']>;
|
||||
title?: InputMaybe<Scalars['String']>;
|
||||
type?: InputMaybe<WidgetType>;
|
||||
};
|
||||
@@ -4723,6 +4748,7 @@ export type UpdatePageLayoutWidgetWithIdInput = {
|
||||
id: Scalars['UUID'];
|
||||
objectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
pageLayoutTabId: Scalars['UUID'];
|
||||
position?: InputMaybe<Scalars['JSON']>;
|
||||
title: Scalars['String'];
|
||||
type: WidgetType;
|
||||
};
|
||||
|
||||
@@ -1028,6 +1028,7 @@ export type CreatePageLayoutWidgetInput = {
|
||||
gridPosition: GridPositionInput;
|
||||
objectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
pageLayoutTabId: Scalars['UUID'];
|
||||
position?: InputMaybe<Scalars['JSON']>;
|
||||
title: Scalars['String'];
|
||||
type: WidgetType;
|
||||
};
|
||||
@@ -3329,11 +3330,34 @@ export type PageLayoutWidget = {
|
||||
id: Scalars['UUID'];
|
||||
objectMetadataId?: Maybe<Scalars['UUID']>;
|
||||
pageLayoutTabId: Scalars['UUID'];
|
||||
position?: Maybe<PageLayoutWidgetPosition>;
|
||||
title: Scalars['String'];
|
||||
type: WidgetType;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type PageLayoutWidgetCanvasPosition = {
|
||||
__typename?: 'PageLayoutWidgetCanvasPosition';
|
||||
layoutMode: PageLayoutTabLayoutMode;
|
||||
};
|
||||
|
||||
export type PageLayoutWidgetGridPosition = {
|
||||
__typename?: 'PageLayoutWidgetGridPosition';
|
||||
column: Scalars['Int'];
|
||||
columnSpan: Scalars['Int'];
|
||||
layoutMode: PageLayoutTabLayoutMode;
|
||||
row: Scalars['Int'];
|
||||
rowSpan: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type PageLayoutWidgetPosition = PageLayoutWidgetCanvasPosition | PageLayoutWidgetGridPosition | PageLayoutWidgetVerticalListPosition;
|
||||
|
||||
export type PageLayoutWidgetVerticalListPosition = {
|
||||
__typename?: 'PageLayoutWidgetVerticalListPosition';
|
||||
index: Scalars['Int'];
|
||||
layoutMode: PageLayoutTabLayoutMode;
|
||||
};
|
||||
|
||||
export type PermissionFlag = {
|
||||
__typename?: 'PermissionFlag';
|
||||
flag: PermissionFlagType;
|
||||
@@ -4539,6 +4563,7 @@ export type UpdatePageLayoutWidgetInput = {
|
||||
configuration?: InputMaybe<Scalars['JSON']>;
|
||||
gridPosition?: InputMaybe<GridPositionInput>;
|
||||
objectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
position?: InputMaybe<Scalars['JSON']>;
|
||||
title?: InputMaybe<Scalars['String']>;
|
||||
type?: InputMaybe<WidgetType>;
|
||||
};
|
||||
@@ -4549,6 +4574,7 @@ export type UpdatePageLayoutWidgetWithIdInput = {
|
||||
id: Scalars['UUID'];
|
||||
objectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
pageLayoutTabId: Scalars['UUID'];
|
||||
position?: InputMaybe<Scalars['JSON']>;
|
||||
title: Scalars['String'];
|
||||
type: WidgetType;
|
||||
};
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddPageLayoutWidgetPositionColumn1770046227329
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddPageLayoutWidgetPositionColumn1770046227329';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."pageLayoutWidget" ADD "position" jsonb`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."pageLayoutWidget" DROP COLUMN "position"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
@@ -5,5 +5,6 @@ export const FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES = [
|
||||
'type',
|
||||
'objectMetadataId',
|
||||
'gridPosition',
|
||||
'position',
|
||||
'configuration',
|
||||
] as const satisfies (keyof FlatPageLayoutWidget)[];
|
||||
|
||||
+1
@@ -42,6 +42,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({
|
||||
type: createPageLayoutWidgetInput.type,
|
||||
objectMetadataId: createPageLayoutWidgetInput.objectMetadataId ?? null,
|
||||
gridPosition: createPageLayoutWidgetInput.gridPosition,
|
||||
position: createPageLayoutWidgetInput.position ?? null,
|
||||
configuration: createPageLayoutWidgetInput.configuration,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
conditionalDisplay: null,
|
||||
|
||||
+6
@@ -11,6 +11,7 @@ import {
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { PageLayoutWidgetPosition } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { GridPositionInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/grid-position.input';
|
||||
@@ -43,6 +44,11 @@ export class CreatePageLayoutWidgetInput {
|
||||
@Type(() => GridPositionInput)
|
||||
gridPosition: GridPositionInput;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
position?: PageLayoutWidgetPosition;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: false })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
|
||||
+6
@@ -11,6 +11,7 @@ import {
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { PageLayoutWidgetPosition } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { GridPositionInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/grid-position.input';
|
||||
@@ -50,6 +51,11 @@ export class UpdatePageLayoutWidgetWithIdInput {
|
||||
@IsNotEmpty()
|
||||
gridPosition: GridPositionInput;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
position?: PageLayoutWidgetPosition;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
|
||||
+6
@@ -10,6 +10,7 @@ import {
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { PageLayoutWidgetPosition } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { GridPositionInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/grid-position.input';
|
||||
@@ -39,6 +40,11 @@ export class UpdatePageLayoutWidgetInput {
|
||||
@IsOptional()
|
||||
gridPosition?: GridPositionInput;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
position?: PageLayoutWidgetPosition;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsIn, IsNotEmpty } from 'class-validator';
|
||||
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
|
||||
@ObjectType('PageLayoutWidgetCanvasPosition')
|
||||
export class PageLayoutWidgetCanvasPositionDTO {
|
||||
@Field(() => PageLayoutTabLayoutMode)
|
||||
@IsIn([PageLayoutTabLayoutMode.CANVAS])
|
||||
@IsNotEmpty()
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsIn, IsInt, IsNotEmpty, Min } from 'class-validator';
|
||||
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
|
||||
@ObjectType('PageLayoutWidgetGridPosition')
|
||||
export class PageLayoutWidgetGridPositionDTO {
|
||||
@Field(() => PageLayoutTabLayoutMode)
|
||||
@IsIn([PageLayoutTabLayoutMode.GRID])
|
||||
@IsNotEmpty()
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID;
|
||||
|
||||
@Field(() => Int)
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsNotEmpty()
|
||||
row: number;
|
||||
|
||||
@Field(() => Int)
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsNotEmpty()
|
||||
column: number;
|
||||
|
||||
@Field(() => Int)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@IsNotEmpty()
|
||||
rowSpan: number;
|
||||
|
||||
@Field(() => Int)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@IsNotEmpty()
|
||||
columnSpan: number;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { createUnionType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
type PageLayoutWidgetPosition,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { PageLayoutWidgetCanvasPositionDTO } from './page-layout-widget-canvas-position.dto';
|
||||
import { PageLayoutWidgetGridPositionDTO } from './page-layout-widget-grid-position.dto';
|
||||
import { PageLayoutWidgetVerticalListPositionDTO } from './page-layout-widget-vertical-list-position.dto';
|
||||
|
||||
export const PageLayoutWidgetPositionUnion = createUnionType({
|
||||
name: 'PageLayoutWidgetPosition',
|
||||
types: () => [
|
||||
PageLayoutWidgetGridPositionDTO,
|
||||
PageLayoutWidgetVerticalListPositionDTO,
|
||||
PageLayoutWidgetCanvasPositionDTO,
|
||||
],
|
||||
resolveType({ layoutMode }: PageLayoutWidgetPosition) {
|
||||
switch (layoutMode) {
|
||||
case PageLayoutTabLayoutMode.GRID:
|
||||
return PageLayoutWidgetGridPositionDTO;
|
||||
case PageLayoutTabLayoutMode.VERTICAL_LIST:
|
||||
return PageLayoutWidgetVerticalListPositionDTO;
|
||||
case PageLayoutTabLayoutMode.CANVAS:
|
||||
return PageLayoutWidgetCanvasPositionDTO;
|
||||
}
|
||||
},
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsIn, IsInt, IsNotEmpty, Min } from 'class-validator';
|
||||
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
|
||||
@ObjectType('PageLayoutWidgetVerticalListPosition')
|
||||
export class PageLayoutWidgetVerticalListPositionDTO {
|
||||
@Field(() => PageLayoutTabLayoutMode)
|
||||
@IsIn([PageLayoutTabLayoutMode.VERTICAL_LIST])
|
||||
@IsNotEmpty()
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST;
|
||||
|
||||
@Field(() => Int)
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsNotEmpty()
|
||||
index: number;
|
||||
}
|
||||
+8
-1
@@ -2,9 +2,13 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import { PageLayoutWidgetConditionalDisplay } from 'twenty-shared/types';
|
||||
import {
|
||||
PageLayoutWidgetConditionalDisplay,
|
||||
PageLayoutWidgetPosition,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { PageLayoutWidgetPositionUnion } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-position.union';
|
||||
import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
|
||||
@@ -46,6 +50,9 @@ export class PageLayoutWidgetDTO {
|
||||
@Field(() => GridPositionDTO, { nullable: false })
|
||||
gridPosition: GridPositionDTO;
|
||||
|
||||
@Field(() => PageLayoutWidgetPositionUnion, { nullable: true })
|
||||
position?: PageLayoutWidgetPosition | null;
|
||||
|
||||
@Field(() => WidgetConfiguration, { nullable: false })
|
||||
configuration: AllPageLayoutWidgetConfiguration;
|
||||
|
||||
|
||||
+7
-1
@@ -1,6 +1,9 @@
|
||||
import { ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { PageLayoutWidgetConditionalDisplay } from 'twenty-shared/types';
|
||||
import {
|
||||
PageLayoutWidgetConditionalDisplay,
|
||||
PageLayoutWidgetPosition,
|
||||
} from 'twenty-shared/types';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@@ -77,6 +80,9 @@ export class PageLayoutWidgetEntity<
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
gridPosition: JsonbProperty<GridPosition>;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
position: JsonbProperty<PageLayoutWidgetPosition | null>;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
configuration: JsonbProperty<
|
||||
PageLayoutWidgetConfigurationTypeSettings<TWidgetConfigurationType>
|
||||
|
||||
+5
-5
@@ -16,7 +16,7 @@ export enum PageLayoutWidgetExceptionMessageKey {
|
||||
PAGE_LAYOUT_TAB_NOT_FOUND = 'PAGE_LAYOUT_TAB_NOT_FOUND',
|
||||
PAGE_LAYOUT_WIDGET_NOT_DELETED = 'PAGE_LAYOUT_WIDGET_NOT_DELETED',
|
||||
GRID_POSITION_REQUIRED = 'GRID_POSITION_REQUIRED',
|
||||
INVALID_WIDGET_GRID_POSITION = 'INVALID_WIDGET_GRID_POSITION',
|
||||
INVALID_WIDGET_POSITION = 'INVALID_WIDGET_POSITION',
|
||||
INVALID_WIDGET_CONFIGURATION = 'INVALID_WIDGET_CONFIGURATION',
|
||||
}
|
||||
|
||||
@@ -66,15 +66,15 @@ export const generatePageLayoutWidgetExceptionMessage = (
|
||||
return 'Page layout widget is not deleted and cannot be restored';
|
||||
case PageLayoutWidgetExceptionMessageKey.GRID_POSITION_REQUIRED:
|
||||
return 'Grid position is required';
|
||||
case PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_GRID_POSITION:
|
||||
case PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_POSITION:
|
||||
if (widgetTitle && detailedError) {
|
||||
return `Invalid grid position for widget "${widgetTitle}": ${detailedError}`;
|
||||
return `Invalid widget position for widget "${widgetTitle}": ${detailedError}`;
|
||||
}
|
||||
if (detailedError) {
|
||||
return `Invalid grid position: ${detailedError}`;
|
||||
return `Invalid widget position: ${detailedError}`;
|
||||
}
|
||||
|
||||
return 'Invalid widget grid position';
|
||||
return 'Invalid widget position';
|
||||
case PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION:
|
||||
if (widgetTitle && widgetType && detailedError) {
|
||||
return `Invalid configuration for widget "${widgetTitle}" of type ${widgetType}: ${detailedError}`;
|
||||
|
||||
+1
-11
@@ -23,7 +23,6 @@ import {
|
||||
generatePageLayoutWidgetExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout-widget/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
|
||||
import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-grid-position.util';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { DashboardSyncService } from 'src/modules/dashboard-sync/services/dashboard-sync.service';
|
||||
@@ -200,16 +199,7 @@ export class PageLayoutWidgetService {
|
||||
const existingFlatPageLayoutWidgetMaps =
|
||||
await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
|
||||
const existingWidget = this.getExistingWidgetOrThrow(
|
||||
id,
|
||||
existingFlatPageLayoutWidgetMaps,
|
||||
);
|
||||
|
||||
if (updateData.gridPosition) {
|
||||
const titleForValidation = updateData.title ?? existingWidget.title;
|
||||
|
||||
validateWidgetGridPosition(updateData.gridPosition, titleForValidation);
|
||||
}
|
||||
this.getExistingWidgetOrThrow(id, existingFlatPageLayoutWidgetMaps);
|
||||
|
||||
const updatePageLayoutWidgetInput: UpdatePageLayoutWidgetInputWithId = {
|
||||
id,
|
||||
|
||||
+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})`,
|
||||
|
||||
+3
@@ -390,6 +390,7 @@ export class PageLayoutUpdateService {
|
||||
type: widgetInput.type,
|
||||
objectMetadataId: widgetInput.objectMetadataId ?? null,
|
||||
gridPosition: widgetInput.gridPosition,
|
||||
position: widgetInput.position ?? null,
|
||||
configuration: widgetInput.configuration ?? null,
|
||||
workspaceId,
|
||||
createdAt: now.toISOString(),
|
||||
@@ -416,6 +417,7 @@ export class PageLayoutUpdateService {
|
||||
type: widgetInput.type,
|
||||
objectMetadataId: widgetInput.objectMetadataId ?? null,
|
||||
gridPosition: widgetInput.gridPosition,
|
||||
position: widgetInput.position ?? null,
|
||||
configuration: widgetInput.configuration ?? null,
|
||||
updatedAt: now.toISOString(),
|
||||
};
|
||||
@@ -436,6 +438,7 @@ export class PageLayoutUpdateService {
|
||||
type: widgetInput.type,
|
||||
objectMetadataId: widgetInput.objectMetadataId ?? null,
|
||||
gridPosition: widgetInput.gridPosition,
|
||||
position: widgetInput.position ?? null,
|
||||
configuration: widgetInput.configuration ?? null,
|
||||
deletedAt: null,
|
||||
updatedAt: now.toISOString(),
|
||||
|
||||
+54
-1
@@ -1,4 +1,9 @@
|
||||
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
type PageLayoutWidgetCanvasPosition,
|
||||
type PageLayoutWidgetGridPosition,
|
||||
type PageLayoutWidgetVerticalListPosition,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { type GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type';
|
||||
@@ -24,6 +29,43 @@ export const GRID_POSITIONS = {
|
||||
},
|
||||
} as const satisfies Record<string, GridPosition>;
|
||||
|
||||
export const GRID_LAYOUT_POSITIONS = {
|
||||
FULL_WIDTH: {
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 12,
|
||||
columnSpan: 12,
|
||||
},
|
||||
HALF_HEIGHT: {
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 6,
|
||||
columnSpan: 12,
|
||||
},
|
||||
RICH_TEXT: {
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 12,
|
||||
column: 0,
|
||||
rowSpan: 6,
|
||||
columnSpan: 12,
|
||||
},
|
||||
} as const satisfies Record<string, PageLayoutWidgetGridPosition>;
|
||||
|
||||
export const VERTICAL_LIST_LAYOUT_POSITIONS = {
|
||||
FIRST: {
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: 0,
|
||||
},
|
||||
} as const satisfies Record<string, PageLayoutWidgetVerticalListPosition>;
|
||||
|
||||
export const CANVAS_LAYOUT_POSITIONS = {
|
||||
DEFAULT: {
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
},
|
||||
} as const satisfies Record<string, PageLayoutWidgetCanvasPosition>;
|
||||
|
||||
export const TAB_PROPS = {
|
||||
home: {
|
||||
title: 'Home',
|
||||
@@ -92,55 +134,66 @@ export const WIDGET_PROPS = {
|
||||
title: 'Fields',
|
||||
type: WidgetType.FIELDS,
|
||||
gridPosition: GRID_POSITIONS.FULL_WIDTH,
|
||||
position: VERTICAL_LIST_LAYOUT_POSITIONS.FIRST,
|
||||
},
|
||||
timeline: {
|
||||
title: 'Timeline',
|
||||
type: WidgetType.TIMELINE,
|
||||
gridPosition: GRID_POSITIONS.HALF_HEIGHT,
|
||||
position: CANVAS_LAYOUT_POSITIONS.DEFAULT,
|
||||
},
|
||||
tasks: {
|
||||
title: 'Tasks',
|
||||
type: WidgetType.TASKS,
|
||||
gridPosition: GRID_POSITIONS.HALF_HEIGHT,
|
||||
position: CANVAS_LAYOUT_POSITIONS.DEFAULT,
|
||||
},
|
||||
notes: {
|
||||
title: 'Notes',
|
||||
type: WidgetType.NOTES,
|
||||
gridPosition: GRID_POSITIONS.HALF_HEIGHT,
|
||||
position: CANVAS_LAYOUT_POSITIONS.DEFAULT,
|
||||
},
|
||||
files: {
|
||||
title: 'Files',
|
||||
type: WidgetType.FILES,
|
||||
gridPosition: GRID_POSITIONS.HALF_HEIGHT,
|
||||
position: CANVAS_LAYOUT_POSITIONS.DEFAULT,
|
||||
},
|
||||
emails: {
|
||||
title: 'Emails',
|
||||
type: WidgetType.EMAILS,
|
||||
gridPosition: GRID_POSITIONS.HALF_HEIGHT,
|
||||
position: CANVAS_LAYOUT_POSITIONS.DEFAULT,
|
||||
},
|
||||
calendar: {
|
||||
title: 'Calendar',
|
||||
type: WidgetType.CALENDAR,
|
||||
gridPosition: GRID_POSITIONS.HALF_HEIGHT,
|
||||
position: CANVAS_LAYOUT_POSITIONS.DEFAULT,
|
||||
},
|
||||
richText: {
|
||||
title: 'Note',
|
||||
type: WidgetType.FIELD_RICH_TEXT,
|
||||
gridPosition: GRID_POSITIONS.RICH_TEXT,
|
||||
position: CANVAS_LAYOUT_POSITIONS.DEFAULT,
|
||||
},
|
||||
workflow: {
|
||||
title: 'Flow',
|
||||
type: WidgetType.WORKFLOW,
|
||||
gridPosition: GRID_POSITIONS.FULL_WIDTH,
|
||||
position: CANVAS_LAYOUT_POSITIONS.DEFAULT,
|
||||
},
|
||||
workflowVersion: {
|
||||
title: 'Flow',
|
||||
type: WidgetType.WORKFLOW_VERSION,
|
||||
gridPosition: GRID_POSITIONS.FULL_WIDTH,
|
||||
position: CANVAS_LAYOUT_POSITIONS.DEFAULT,
|
||||
},
|
||||
workflowRun: {
|
||||
title: 'Flow',
|
||||
type: WidgetType.WORKFLOW_RUN,
|
||||
gridPosition: GRID_POSITIONS.FULL_WIDTH,
|
||||
position: CANVAS_LAYOUT_POSITIONS.DEFAULT,
|
||||
},
|
||||
} as const;
|
||||
|
||||
+6
-1
@@ -1,4 +1,7 @@
|
||||
import { type PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
import {
|
||||
type PageLayoutTabLayoutMode,
|
||||
type PageLayoutWidgetPosition,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { type GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type';
|
||||
@@ -8,6 +11,7 @@ export type StandardPageLayoutWidgetDefinition = {
|
||||
title?: string;
|
||||
type?: WidgetType;
|
||||
gridPosition?: GridPosition;
|
||||
position?: PageLayoutWidgetPosition;
|
||||
};
|
||||
|
||||
export type StandardPageLayoutTabDefinition = {
|
||||
@@ -24,6 +28,7 @@ export type StandardRecordPageWidgetDefinition = {
|
||||
title: string;
|
||||
type: WidgetType;
|
||||
gridPosition: GridPosition;
|
||||
position?: PageLayoutWidgetPosition;
|
||||
};
|
||||
|
||||
export type StandardRecordPageTabDefinition = {
|
||||
|
||||
+4
-1
@@ -21,7 +21,10 @@ const NOTE_PAGE_TABS = {
|
||||
},
|
||||
richText: {
|
||||
universalIdentifier: '20202020-ac04-4004-8004-a0be5a11a412',
|
||||
...WIDGET_PROPS.richText,
|
||||
title: WIDGET_PROPS.richText.title,
|
||||
type: WIDGET_PROPS.richText.type,
|
||||
gridPosition: WIDGET_PROPS.richText.gridPosition,
|
||||
position: { layoutMode: TAB_PROPS.home.layoutMode, index: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+6
-2
@@ -1,14 +1,18 @@
|
||||
import { type PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
import {
|
||||
type PageLayoutTabLayoutMode,
|
||||
type PageLayoutWidgetPosition,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import { type WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { type GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type';
|
||||
import { type PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
|
||||
export type StandardPageLayoutWidgetConfig = {
|
||||
universalIdentifier: string;
|
||||
title?: string;
|
||||
type?: WidgetType;
|
||||
gridPosition?: GridPosition;
|
||||
position?: PageLayoutWidgetPosition;
|
||||
};
|
||||
|
||||
export type StandardPageLayoutTabConfig = {
|
||||
|
||||
+4
-1
@@ -21,7 +21,10 @@ const TASK_PAGE_TABS = {
|
||||
},
|
||||
richText: {
|
||||
universalIdentifier: '20202020-ac05-4005-8005-ba5ca11a5512',
|
||||
...WIDGET_PROPS.richText,
|
||||
title: WIDGET_PROPS.richText.title,
|
||||
type: WIDGET_PROPS.richText.type,
|
||||
gridPosition: WIDGET_PROPS.richText.gridPosition,
|
||||
position: { layoutMode: TAB_PROPS.home.layoutMode, index: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+1
@@ -88,6 +88,7 @@ const computeRecordPageWidgets = ({
|
||||
title: widget.title,
|
||||
type: widget.type,
|
||||
gridPosition: widget.gridPosition,
|
||||
position: widget.position ?? null,
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
},
|
||||
|
||||
+57
@@ -1,4 +1,5 @@
|
||||
import { CalendarStartDay } from 'twenty-shared/constants';
|
||||
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
@@ -65,6 +66,13 @@ const createWelcomeRichText = ({
|
||||
title: 'Untitled Rich Text',
|
||||
type: WidgetType.STANDALONE_RICH_TEXT,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 6, columnSpan: 6 },
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 6,
|
||||
columnSpan: 6,
|
||||
},
|
||||
configuration: {
|
||||
configurationType:
|
||||
WidgetConfigurationType.STANDALONE_RICH_TEXT as const,
|
||||
@@ -232,6 +240,13 @@ const createDealsByCompany = ({
|
||||
title: 'Deals by Company',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 6, rowSpan: 6, columnSpan: 6 },
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 0,
|
||||
column: 6,
|
||||
rowSpan: 6,
|
||||
columnSpan: 6,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.PIE_CHART,
|
||||
groupByFieldMetadataId: opportunityFields.company.id,
|
||||
@@ -270,6 +285,13 @@ const createPipelineValueByStage = ({
|
||||
title: 'Pipeline Value by Stage',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 6, column: 0, rowSpan: 6, columnSpan: 6 },
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 6,
|
||||
column: 0,
|
||||
rowSpan: 6,
|
||||
columnSpan: 6,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.BAR_CHART,
|
||||
aggregateFieldMetadataId: opportunityFields.amount.id,
|
||||
@@ -314,6 +336,13 @@ const createRevenueTimeline = ({
|
||||
title: 'Revenue Timeline',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 6, column: 6, rowSpan: 6, columnSpan: 6 },
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 6,
|
||||
column: 6,
|
||||
rowSpan: 6,
|
||||
columnSpan: 6,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.LINE_CHART,
|
||||
aggregateFieldMetadataId: opportunityFields.amount.id,
|
||||
@@ -353,6 +382,13 @@ const createOpportunitiesByOwner = ({
|
||||
title: 'Opportunities by Owner',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 12, column: 0, rowSpan: 6, columnSpan: 6 },
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 12,
|
||||
column: 0,
|
||||
rowSpan: 6,
|
||||
columnSpan: 6,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.BAR_CHART,
|
||||
aggregateFieldMetadataId: opportunityFields.id.id,
|
||||
@@ -394,6 +430,13 @@ const createStockMarketIframe = ({
|
||||
title: 'Stock market (Iframe)',
|
||||
type: WidgetType.IFRAME,
|
||||
gridPosition: { row: 12, column: 6, rowSpan: 8, columnSpan: 6 },
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 12,
|
||||
column: 6,
|
||||
rowSpan: 8,
|
||||
columnSpan: 6,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.IFRAME as const,
|
||||
url: 'https://www.tradingview.com/embed-widget/hotlists/?locale=en',
|
||||
@@ -421,6 +464,13 @@ const createDealsCreatedThisMonth = ({
|
||||
title: 'Deals created this month',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 18, column: 0, rowSpan: 2, columnSpan: 3 },
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 18,
|
||||
column: 0,
|
||||
rowSpan: 2,
|
||||
columnSpan: 3,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.AGGREGATE_CHART,
|
||||
aggregateFieldMetadataId: opportunityFields.id.id,
|
||||
@@ -464,6 +514,13 @@ const createDealValueCreatedThisMonth = ({
|
||||
title: 'Deal value created this month',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 18, column: 3, rowSpan: 2, columnSpan: 3 },
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 18,
|
||||
column: 3,
|
||||
rowSpan: 2,
|
||||
columnSpan: 3,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.AGGREGATE_CHART,
|
||||
aggregateFieldMetadataId: opportunityFields.amount.id,
|
||||
|
||||
+4
@@ -1,3 +1,4 @@
|
||||
import { type PageLayoutWidgetPosition } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
@@ -19,6 +20,7 @@ export type CreateStandardPageLayoutWidgetContext = {
|
||||
title: string;
|
||||
type: WidgetType;
|
||||
gridPosition: GridPosition;
|
||||
position: PageLayoutWidgetPosition | null;
|
||||
configuration: AllPageLayoutWidgetConfiguration;
|
||||
objectMetadataId: string | null;
|
||||
};
|
||||
@@ -40,6 +42,7 @@ export const createStandardPageLayoutWidgetFlatMetadata = ({
|
||||
title,
|
||||
type,
|
||||
gridPosition,
|
||||
position,
|
||||
configuration,
|
||||
objectMetadataId,
|
||||
},
|
||||
@@ -73,6 +76,7 @@ export const createStandardPageLayoutWidgetFlatMetadata = ({
|
||||
title,
|
||||
type,
|
||||
gridPosition,
|
||||
position,
|
||||
configuration,
|
||||
objectMetadataId,
|
||||
createdAt: now,
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,12 @@ export { ObjectRecordGroupByDateGranularity } from './ObjectRecordGroupByDateGra
|
||||
export type { ObjectsPermissions } from './ObjectsPermissions';
|
||||
export type { ObjectsPermissionsByRoleId } from './ObjectsPermissionsByRoleId';
|
||||
export type { OrderBy } from './OrderBy';
|
||||
export type {
|
||||
PageLayoutWidgetGridPosition,
|
||||
PageLayoutWidgetVerticalListPosition,
|
||||
PageLayoutWidgetCanvasPosition,
|
||||
PageLayoutWidgetPosition,
|
||||
} from './page-layout/page-layout-widget-position.type';
|
||||
export { PageLayoutTabLayoutMode } from './page-layout/PageLayoutTabLayoutMode';
|
||||
export type { PageLayoutWidgetConditionalDisplay } from './page-layout/PageLayoutWidgetConditionalDisplay';
|
||||
export type { PartialFieldMetadataItem } from './PartialFieldMetadataItem';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { PageLayoutTabLayoutMode } from './PageLayoutTabLayoutMode';
|
||||
|
||||
export type PageLayoutWidgetGridPosition = {
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID;
|
||||
row: number;
|
||||
column: number;
|
||||
rowSpan: number;
|
||||
columnSpan: number;
|
||||
};
|
||||
|
||||
export type PageLayoutWidgetVerticalListPosition = {
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export type PageLayoutWidgetCanvasPosition = {
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS;
|
||||
};
|
||||
|
||||
export type PageLayoutWidgetPosition =
|
||||
| PageLayoutWidgetGridPosition
|
||||
| PageLayoutWidgetVerticalListPosition
|
||||
| PageLayoutWidgetCanvasPosition;
|
||||
Reference in New Issue
Block a user