Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
956419dc60 | ||
|
|
ad0662ddb1 |
+1
-1
@@ -930,7 +930,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
universalProperty: undefined,
|
||||
},
|
||||
pageLayoutTabId: {
|
||||
toCompare: false,
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: 'pageLayoutTabUniversalIdentifier',
|
||||
isOverridable: true,
|
||||
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-tab.type';
|
||||
import { FlatPageLayoutWidgetValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service';
|
||||
|
||||
const CANVAS_TAB_UI = '00000000-0000-0000-0000-000000000aa1';
|
||||
const GRID_TAB_UI = '00000000-0000-0000-0000-000000000aa2';
|
||||
const WIDGET_A_UI = '00000000-0000-0000-0000-000000000111';
|
||||
const WIDGET_B_UI = '00000000-0000-0000-0000-000000000222';
|
||||
|
||||
const CANVAS_CAP_ERROR = /CANVAS layout tab can only contain one widget/;
|
||||
|
||||
const tab = (
|
||||
layoutMode: PageLayoutTabLayoutMode,
|
||||
universalIdentifier: string,
|
||||
): UniversalFlatPageLayoutTab =>
|
||||
({
|
||||
universalIdentifier,
|
||||
layoutMode,
|
||||
}) as unknown as UniversalFlatPageLayoutTab;
|
||||
|
||||
const widget = (
|
||||
universalIdentifier: string,
|
||||
pageLayoutTabUniversalIdentifier: string,
|
||||
options?: {
|
||||
universalOverrides?: {
|
||||
pageLayoutTabUniversalIdentifier?: string;
|
||||
} | null;
|
||||
},
|
||||
): FlatPageLayoutWidget =>
|
||||
({
|
||||
universalIdentifier,
|
||||
pageLayoutTabUniversalIdentifier,
|
||||
universalOverrides: options?.universalOverrides ?? null,
|
||||
title: 'widget',
|
||||
type: 'FRONT_COMPONENT',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 },
|
||||
position: { layoutMode: PageLayoutTabLayoutMode.CANVAS },
|
||||
}) as unknown as FlatPageLayoutWidget;
|
||||
|
||||
const mapsFrom = (entities: { universalIdentifier: string }[]): any => {
|
||||
const maps = createEmptyFlatEntityMaps() as any;
|
||||
|
||||
for (const entity of entities) {
|
||||
maps.byUniversalIdentifier[entity.universalIdentifier] = entity;
|
||||
}
|
||||
|
||||
return maps;
|
||||
};
|
||||
|
||||
const buildCreateArgs = ({
|
||||
incoming,
|
||||
tabs,
|
||||
existingWidgets,
|
||||
}: {
|
||||
incoming: FlatPageLayoutWidget;
|
||||
tabs: UniversalFlatPageLayoutTab[];
|
||||
existingWidgets: FlatPageLayoutWidget[];
|
||||
}) =>
|
||||
({
|
||||
flatEntityToValidate: incoming,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatPageLayoutTabMaps: mapsFrom(tabs),
|
||||
flatPageLayoutWidgetMaps: mapsFrom(existingWidgets),
|
||||
},
|
||||
additionalCacheDataMaps: { featureFlagsMap: {} },
|
||||
workspaceId: 'workspace-id',
|
||||
buildOptions: {} as never,
|
||||
}) as any;
|
||||
|
||||
const buildUpdateArgs = ({
|
||||
universalIdentifier,
|
||||
update,
|
||||
tabs,
|
||||
existingWidgets,
|
||||
}: {
|
||||
universalIdentifier: string;
|
||||
update: Record<string, unknown>;
|
||||
tabs: UniversalFlatPageLayoutTab[];
|
||||
existingWidgets: FlatPageLayoutWidget[];
|
||||
}) =>
|
||||
({
|
||||
universalIdentifier,
|
||||
flatEntityUpdate: update,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatPageLayoutTabMaps: mapsFrom(tabs),
|
||||
flatPageLayoutWidgetMaps: mapsFrom(existingWidgets),
|
||||
},
|
||||
additionalCacheDataMaps: { featureFlagsMap: {} },
|
||||
workspaceId: 'workspace-id',
|
||||
buildOptions: {} as never,
|
||||
}) as any;
|
||||
|
||||
const errorsMatching = (errors: { message: string }[], matcher: RegExp) =>
|
||||
errors.filter((error) => matcher.test(error.message));
|
||||
|
||||
describe('FlatPageLayoutWidgetValidatorService — CANVAS widget cap', () => {
|
||||
let service: FlatPageLayoutWidgetValidatorService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleRef: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
FlatPageLayoutWidgetValidatorService,
|
||||
{
|
||||
provide: FlatPageLayoutWidgetTypeValidatorService,
|
||||
useValue: {
|
||||
validateFlatPageLayoutWidgetTypeSpecificitiesForCreation: () => [],
|
||||
validateFlatPageLayoutWidgetTypeSpecificitiesForUpdate: () => [],
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = moduleRef.get(FlatPageLayoutWidgetValidatorService);
|
||||
});
|
||||
|
||||
it('rejects a second widget on a CANVAS tab', async () => {
|
||||
const canvasTab = tab(PageLayoutTabLayoutMode.CANVAS, CANVAS_TAB_UI);
|
||||
|
||||
const result = await service.validateFlatPageLayoutWidgetCreation(
|
||||
buildCreateArgs({
|
||||
incoming: widget(WIDGET_B_UI, CANVAS_TAB_UI),
|
||||
tabs: [canvasTab],
|
||||
existingWidgets: [widget(WIDGET_A_UI, CANVAS_TAB_UI)],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(errorsMatching(result.errors, CANVAS_CAP_ERROR)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('counts widgets assigned to a CANVAS tab through overrides', async () => {
|
||||
const canvasTab = tab(PageLayoutTabLayoutMode.CANVAS, CANVAS_TAB_UI);
|
||||
const gridTab = tab(PageLayoutTabLayoutMode.GRID, GRID_TAB_UI);
|
||||
|
||||
const result = await service.validateFlatPageLayoutWidgetCreation(
|
||||
buildCreateArgs({
|
||||
incoming: widget(WIDGET_B_UI, CANVAS_TAB_UI),
|
||||
tabs: [canvasTab, gridTab],
|
||||
existingWidgets: [
|
||||
widget(WIDGET_A_UI, GRID_TAB_UI, {
|
||||
universalOverrides: {
|
||||
pageLayoutTabUniversalIdentifier: CANVAS_TAB_UI,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(errorsMatching(result.errors, CANVAS_CAP_ERROR)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('rejects moving a widget into an occupied CANVAS tab', async () => {
|
||||
const canvasTab = tab(PageLayoutTabLayoutMode.CANVAS, CANVAS_TAB_UI);
|
||||
const gridTab = tab(PageLayoutTabLayoutMode.GRID, GRID_TAB_UI);
|
||||
|
||||
const result = await service.validateFlatPageLayoutWidgetUpdate(
|
||||
buildUpdateArgs({
|
||||
universalIdentifier: WIDGET_B_UI,
|
||||
update: {
|
||||
universalOverrides: {
|
||||
pageLayoutTabUniversalIdentifier: CANVAS_TAB_UI,
|
||||
},
|
||||
},
|
||||
tabs: [canvasTab, gridTab],
|
||||
existingWidgets: [
|
||||
widget(WIDGET_A_UI, CANVAS_TAB_UI),
|
||||
widget(WIDGET_B_UI, GRID_TAB_UI),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(errorsMatching(result.errors, CANVAS_CAP_ERROR)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not apply the cap to non-CANVAS tabs', async () => {
|
||||
const gridTab = tab(PageLayoutTabLayoutMode.GRID, GRID_TAB_UI);
|
||||
|
||||
const result = await service.validateFlatPageLayoutWidgetCreation(
|
||||
buildCreateArgs({
|
||||
incoming: widget(WIDGET_B_UI, GRID_TAB_UI),
|
||||
tabs: [gridTab],
|
||||
existingWidgets: [widget(WIDGET_A_UI, GRID_TAB_UI)],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(errorsMatching(result.errors, CANVAS_CAP_ERROR)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
+81
@@ -13,10 +13,12 @@ import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules
|
||||
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 { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { type MetadataUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-universal-flat-entity-maps.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 { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-tab.type';
|
||||
import { type UniversalFlatPageLayoutWidget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-widget.type';
|
||||
import {
|
||||
FailedFlatEntityValidation,
|
||||
FlatEntityValidationError,
|
||||
@@ -83,6 +85,23 @@ export class FlatPageLayoutWidgetValidatorService {
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutTabMaps,
|
||||
});
|
||||
|
||||
const effectivePageLayoutTab = findFlatEntityByUniversalIdentifier({
|
||||
universalIdentifier: this.getEffectivePageLayoutTabUniversalIdentifier(
|
||||
updatedFlatPageLayoutWidget,
|
||||
),
|
||||
flatEntityMaps:
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutTabMaps,
|
||||
});
|
||||
|
||||
const canvasTabErrors = this.validateCanvasTabWidgetCap({
|
||||
pageLayoutTab: effectivePageLayoutTab,
|
||||
flatPageLayoutWidgetMaps:
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutWidgetMaps,
|
||||
excludeWidgetUniversalIdentifier: universalIdentifier,
|
||||
});
|
||||
|
||||
validationResult.errors.push(...canvasTabErrors);
|
||||
|
||||
const gridPositionErrors = this.validateGridPosition({
|
||||
gridPosition: updatedFlatPageLayoutWidget.gridPosition,
|
||||
widgetTitle: updatedFlatPageLayoutWidget.title,
|
||||
@@ -203,6 +222,16 @@ export class FlatPageLayoutWidgetValidatorService {
|
||||
});
|
||||
}
|
||||
|
||||
const canvasTabErrors = this.validateCanvasTabWidgetCap({
|
||||
pageLayoutTab: referencedPageLayoutTab,
|
||||
flatPageLayoutWidgetMaps:
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutWidgetMaps,
|
||||
excludeWidgetUniversalIdentifier:
|
||||
flatPageLayoutWidgetToValidate.universalIdentifier,
|
||||
});
|
||||
|
||||
validationResult.errors.push(...canvasTabErrors);
|
||||
|
||||
const gridPositionErrors = this.validateGridPosition({
|
||||
gridPosition: flatPageLayoutWidgetToValidate.gridPosition,
|
||||
widgetTitle: flatPageLayoutWidgetToValidate.title,
|
||||
@@ -235,6 +264,58 @@ export class FlatPageLayoutWidgetValidatorService {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
private validateCanvasTabWidgetCap({
|
||||
pageLayoutTab,
|
||||
flatPageLayoutWidgetMaps,
|
||||
excludeWidgetUniversalIdentifier,
|
||||
}: {
|
||||
pageLayoutTab: UniversalFlatPageLayoutTab | undefined;
|
||||
flatPageLayoutWidgetMaps: MetadataUniversalFlatEntityMaps<'pageLayoutWidget'>;
|
||||
excludeWidgetUniversalIdentifier?: string;
|
||||
}): FlatEntityValidationError[] {
|
||||
if (
|
||||
!isDefined(pageLayoutTab) ||
|
||||
pageLayoutTab.layoutMode !== PageLayoutTabLayoutMode.CANVAS
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const existingWidgetsOnTab = Object.values(
|
||||
flatPageLayoutWidgetMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(widget) =>
|
||||
isDefined(widget) &&
|
||||
!isDefined(widget.deletedAt) &&
|
||||
this.getEffectivePageLayoutTabUniversalIdentifier(widget) ===
|
||||
pageLayoutTab.universalIdentifier &&
|
||||
widget.universalIdentifier !== excludeWidgetUniversalIdentifier,
|
||||
);
|
||||
|
||||
if (existingWidgetsOnTab.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
code: PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
message: t`A CANVAS layout tab can only contain one widget`,
|
||||
userFriendlyMessage: msg`A CANVAS layout tab can only contain one widget`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private getEffectivePageLayoutTabUniversalIdentifier(
|
||||
widget: Pick<
|
||||
UniversalFlatPageLayoutWidget,
|
||||
'pageLayoutTabUniversalIdentifier' | 'universalOverrides'
|
||||
>,
|
||||
): string {
|
||||
return (
|
||||
widget.universalOverrides?.pageLayoutTabUniversalIdentifier ??
|
||||
widget.pageLayoutTabUniversalIdentifier
|
||||
);
|
||||
}
|
||||
|
||||
private validateGridPosition({
|
||||
gridPosition,
|
||||
widgetTitle,
|
||||
|
||||
+37
-1
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Page layout widget creation should fail AGGREGATE_CHART widget configuration validation failures when AGGREGATE_CHART configuration has invalid UUID 1`] = `
|
||||
{
|
||||
@@ -44,6 +44,42 @@ exports[`Page layout widget creation should fail BAR_CHART widget configuration
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Page layout widget creation should fail CANVAS tab widget cap rejects a second widget on a CANVAS tab 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"pageLayoutWidget": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_PAGE_LAYOUT_WIDGET_DATA",
|
||||
"message": "A CANVAS layout tab can only contain one widget",
|
||||
"userFriendlyMessage": "A CANVAS layout tab can only contain one widget",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"pageLayoutTabUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "pageLayoutWidget",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 pageLayoutWidget",
|
||||
"summary": {
|
||||
"pageLayoutWidget": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "A CANVAS layout tab can only contain one widget",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating page layout widget",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Page layout widget creation should fail Edge case configuration validation failures when configuration has missing configurationType 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
|
||||
+51
@@ -16,6 +16,7 @@ import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-l
|
||||
import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util';
|
||||
import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util';
|
||||
import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
|
||||
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
|
||||
import { type CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
@@ -257,6 +258,56 @@ describe('Page layout widget creation should fail', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('CANVAS tab widget cap', () => {
|
||||
let canvasTabId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data } = await createOnePageLayoutTab({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
title: 'CANVAS Tab For Cap Test',
|
||||
pageLayoutId: testPageLayoutId,
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
},
|
||||
});
|
||||
|
||||
canvasTabId = data.createPageLayoutTab.id;
|
||||
|
||||
await createOnePageLayoutWidget({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
title: 'First widget on CANVAS tab',
|
||||
pageLayoutTabId: canvasTabId,
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: TEST_IFRAME_CONFIG,
|
||||
gridPosition: DEFAULT_GRID_POSITION,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await destroyOnePageLayoutTab({
|
||||
expectToFail: false,
|
||||
input: { id: canvasTabId },
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a second widget on a CANVAS tab', async () => {
|
||||
const { errors } = await createOnePageLayoutWidget({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
title: 'Second widget should be rejected',
|
||||
pageLayoutTabId: canvasTabId,
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: TEST_IFRAME_CONFIG,
|
||||
gridPosition: DEFAULT_GRID_POSITION,
|
||||
},
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge case configuration validation failures', () => {
|
||||
it('when configuration is null', async () => {
|
||||
const { errors } = await createOnePageLayoutWidget({
|
||||
|
||||
+173
-1
@@ -11,7 +11,11 @@ import {
|
||||
import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util';
|
||||
import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
|
||||
import { updateOnePageLayoutWithTabsAndWidgets } from 'test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets.util';
|
||||
import { AggregateOperations, FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
AggregateOperations,
|
||||
FieldMetadataType,
|
||||
PageLayoutTabLayoutMode,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -58,6 +62,174 @@ describe('Page layout with tabs update should fail', () => {
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
});
|
||||
|
||||
it('when moving a widget into an occupied CANVAS tab', async () => {
|
||||
const { data: layoutData } = await createOnePageLayout({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'Move Into Occupied Canvas Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
},
|
||||
});
|
||||
|
||||
const testPageLayoutId = layoutData.createPageLayout.id;
|
||||
const sourceTabId = v4();
|
||||
const canvasTabId = v4();
|
||||
const widgetToMoveId = v4();
|
||||
const canvasWidgetId = v4();
|
||||
|
||||
try {
|
||||
await updateOnePageLayoutWithTabsAndWidgets({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
id: testPageLayoutId,
|
||||
name: 'Move Into Occupied Canvas Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
id: sourceTabId,
|
||||
title: 'Source Tab',
|
||||
position: 0,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
widgets: [
|
||||
{
|
||||
id: widgetToMoveId,
|
||||
pageLayoutTabId: sourceTabId,
|
||||
title: 'Widget To Move',
|
||||
type: WidgetType.IFRAME,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: 0,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.IFRAME,
|
||||
url: 'https://example.com/source',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: canvasTabId,
|
||||
title: 'Canvas Tab',
|
||||
position: 1,
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
id: canvasWidgetId,
|
||||
pageLayoutTabId: canvasTabId,
|
||||
title: 'Canvas Widget',
|
||||
type: WidgetType.IFRAME,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.IFRAME,
|
||||
url: 'https://example.com/canvas',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { errors } = await updateOnePageLayoutWithTabsAndWidgets({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
id: testPageLayoutId,
|
||||
name: 'Move Into Occupied Canvas Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
id: sourceTabId,
|
||||
title: 'Source Tab',
|
||||
position: 0,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
widgets: [],
|
||||
},
|
||||
{
|
||||
id: canvasTabId,
|
||||
title: 'Canvas Tab',
|
||||
position: 1,
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
id: canvasWidgetId,
|
||||
pageLayoutTabId: canvasTabId,
|
||||
title: 'Canvas Widget',
|
||||
type: WidgetType.IFRAME,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.IFRAME,
|
||||
url: 'https://example.com/canvas',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: widgetToMoveId,
|
||||
pageLayoutTabId: canvasTabId,
|
||||
title: 'Widget To Move',
|
||||
type: WidgetType.IFRAME,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.IFRAME,
|
||||
url: 'https://example.com/source',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toHaveLength(1);
|
||||
const [firstError] = errors ?? [];
|
||||
|
||||
expect(firstError?.extensions.code).toBe('BAD_USER_INPUT');
|
||||
expect(firstError?.message).toContain(
|
||||
'A CANVAS layout tab can only contain one widget',
|
||||
);
|
||||
} finally {
|
||||
await destroyOnePageLayout({
|
||||
expectToFail: false,
|
||||
input: { id: testPageLayoutId },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('chart filter validation failures', () => {
|
||||
let testFieldMetadataIds: TestFieldMetadataIds;
|
||||
let testPageLayoutId: string | undefined;
|
||||
|
||||
+110
-1
@@ -8,7 +8,10 @@ import {
|
||||
type EachTestingContext,
|
||||
eachTestingContextFilter,
|
||||
} from 'twenty-shared/testing';
|
||||
import { AggregateOperations } from 'twenty-shared/types';
|
||||
import {
|
||||
AggregateOperations,
|
||||
PageLayoutTabLayoutMode,
|
||||
} from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
@@ -229,4 +232,110 @@ describe('Page layout with tabs update should succeed', () => {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('should move a widget to another tab when saving layout tabs and widgets', async () => {
|
||||
const widgetId = v4();
|
||||
|
||||
await updateOnePageLayoutWithTabsAndWidgets({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
id: testPageLayoutId,
|
||||
name: 'Layout Before Widget Move',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
id: testTabId1,
|
||||
title: 'Source Tab',
|
||||
position: 0,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
widgets: [
|
||||
{
|
||||
id: widgetId,
|
||||
pageLayoutTabId: testTabId1,
|
||||
title: 'Iframe Widget',
|
||||
type: WidgetType.IFRAME,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: 0,
|
||||
},
|
||||
configuration: MOCK_IFRAME_CONFIGURATION,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: testTabId2,
|
||||
title: 'Destination Tab',
|
||||
position: 1,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
widgets: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await updateOnePageLayoutWithTabsAndWidgets({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
id: testPageLayoutId,
|
||||
name: 'Layout After Widget Move',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
id: testTabId1,
|
||||
title: 'Source Tab',
|
||||
position: 0,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
widgets: [],
|
||||
},
|
||||
{
|
||||
id: testTabId2,
|
||||
title: 'Destination Tab',
|
||||
position: 1,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
widgets: [
|
||||
{
|
||||
id: widgetId,
|
||||
pageLayoutTabId: testTabId2,
|
||||
title: 'Iframe Widget',
|
||||
type: WidgetType.IFRAME,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 1,
|
||||
},
|
||||
position: {
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: 0,
|
||||
},
|
||||
configuration: MOCK_IFRAME_CONFIGURATION,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const updatedTabs = data.updatePageLayoutWithTabsAndWidgets.tabs ?? [];
|
||||
const sourceTab = updatedTabs.find((tab) => tab.id === testTabId1);
|
||||
const destinationTab = updatedTabs.find((tab) => tab.id === testTabId2);
|
||||
|
||||
expect(sourceTab?.widgets).toHaveLength(0);
|
||||
expect(destinationTab?.widgets).toHaveLength(1);
|
||||
expect(destinationTab?.widgets?.[0]).toMatchObject({
|
||||
id: widgetId,
|
||||
pageLayoutTabId: testTabId2,
|
||||
title: 'Iframe Widget',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user