Fix page layout widget tab moves (#20915)
Fixes widget moves between page layout tabs by making pageLayoutTabId part of the flat-entity diff, so the save mutation no longer silently drops the new tab assignment. https://discord.com/channels/1130383047699738754/1508737039128985680
This commit is contained in:
+146
@@ -0,0 +1,146 @@
|
||||
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 { PageLayoutTabExceptionCode } from 'src/engine/metadata-modules/page-layout-tab/exceptions/page-layout-tab.exception';
|
||||
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 EXISTING_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'00000000-0000-0000-0000-000000000aa1';
|
||||
const MISSING_TAB_UNIVERSAL_IDENTIFIER = '00000000-0000-0000-0000-000000000aa2';
|
||||
const DESTINATION_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'00000000-0000-0000-0000-000000000aa3';
|
||||
const WIDGET_UNIVERSAL_IDENTIFIER = '00000000-0000-0000-0000-000000000111';
|
||||
|
||||
const tab = (
|
||||
universalIdentifier = EXISTING_TAB_UNIVERSAL_IDENTIFIER,
|
||||
): UniversalFlatPageLayoutTab =>
|
||||
({
|
||||
universalIdentifier,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
}) as unknown as UniversalFlatPageLayoutTab;
|
||||
|
||||
const widget = (
|
||||
universalIdentifier = WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
pageLayoutTabUniversalIdentifier = EXISTING_TAB_UNIVERSAL_IDENTIFIER,
|
||||
): FlatPageLayoutWidget =>
|
||||
({
|
||||
universalIdentifier,
|
||||
pageLayoutTabUniversalIdentifier,
|
||||
title: 'widget',
|
||||
type: 'FRONT_COMPONENT',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 },
|
||||
position: { layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, index: 0 },
|
||||
}) 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 buildUpdateArgs = ({
|
||||
update,
|
||||
tabs = [tab()],
|
||||
existingWidgets = [widget()],
|
||||
}: {
|
||||
update: Record<string, unknown>;
|
||||
tabs?: UniversalFlatPageLayoutTab[];
|
||||
existingWidgets?: FlatPageLayoutWidget[];
|
||||
}) =>
|
||||
({
|
||||
universalIdentifier: WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
flatEntityUpdate: update,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatPageLayoutTabMaps: mapsFrom(tabs),
|
||||
flatPageLayoutWidgetMaps: mapsFrom(existingWidgets),
|
||||
},
|
||||
additionalCacheDataMaps: { featureFlagsMap: {} },
|
||||
workspaceId: 'workspace-id',
|
||||
buildOptions: {} as never,
|
||||
}) as unknown as Parameters<
|
||||
FlatPageLayoutWidgetValidatorService['validateFlatPageLayoutWidgetUpdate']
|
||||
>[0];
|
||||
|
||||
describe('FlatPageLayoutWidgetValidatorService', () => {
|
||||
let service: FlatPageLayoutWidgetValidatorService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleRef: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
FlatPageLayoutWidgetValidatorService,
|
||||
{
|
||||
provide: FlatPageLayoutWidgetTypeValidatorService,
|
||||
useValue: {
|
||||
validateFlatPageLayoutWidgetTypeSpecificitiesForCreation: () => [],
|
||||
validateFlatPageLayoutWidgetTypeSpecificitiesForUpdate: () => [],
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = moduleRef.get(FlatPageLayoutWidgetValidatorService);
|
||||
});
|
||||
|
||||
describe('validateFlatPageLayoutWidgetUpdate', () => {
|
||||
it('rejects moving a widget to an unknown tab', async () => {
|
||||
const result = await service.validateFlatPageLayoutWidgetUpdate(
|
||||
buildUpdateArgs({
|
||||
update: {
|
||||
pageLayoutTabUniversalIdentifier: MISSING_TAB_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.errors.map((error) => error.code)).toContain(
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects moving an overridden widget to an unknown tab', async () => {
|
||||
const result = await service.validateFlatPageLayoutWidgetUpdate(
|
||||
buildUpdateArgs({
|
||||
update: {
|
||||
universalOverrides: {
|
||||
pageLayoutTabUniversalIdentifier:
|
||||
MISSING_TAB_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.errors.map((error) => error.code)).toContain(
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts moving an overridden widget to a known tab and exposes the override target as the effective tab', async () => {
|
||||
const result = await service.validateFlatPageLayoutWidgetUpdate(
|
||||
buildUpdateArgs({
|
||||
update: {
|
||||
universalOverrides: {
|
||||
pageLayoutTabUniversalIdentifier:
|
||||
DESTINATION_TAB_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
tabs: [tab(), tab(DESTINATION_TAB_UNIVERSAL_IDENTIFIER)],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.errors.map((error) => error.code)).not.toContain(
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
);
|
||||
expect(
|
||||
result.flatEntityMinimalInformation.pageLayoutTabUniversalIdentifier,
|
||||
).toBe(DESTINATION_TAB_UNIVERSAL_IDENTIFIER);
|
||||
});
|
||||
});
|
||||
});
|
||||
+28
-3
@@ -17,6 +17,7 @@ import { validatePageLayoutWidgetGridPosition } from 'src/engine/metadata-module
|
||||
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,
|
||||
@@ -70,19 +71,31 @@ export class FlatPageLayoutWidgetValidatorService {
|
||||
...flatEntityUpdate,
|
||||
};
|
||||
|
||||
const effectivePageLayoutTabUniversalIdentifier =
|
||||
this.getEffectivePageLayoutTabUniversalIdentifier(
|
||||
updatedFlatPageLayoutWidget,
|
||||
);
|
||||
|
||||
validationResult.flatEntityMinimalInformation = {
|
||||
...validationResult.flatEntityMinimalInformation,
|
||||
pageLayoutTabUniversalIdentifier:
|
||||
updatedFlatPageLayoutWidget.pageLayoutTabUniversalIdentifier,
|
||||
effectivePageLayoutTabUniversalIdentifier,
|
||||
};
|
||||
|
||||
const referencedPageLayoutTab = findFlatEntityByUniversalIdentifier({
|
||||
universalIdentifier:
|
||||
updatedFlatPageLayoutWidget.pageLayoutTabUniversalIdentifier,
|
||||
universalIdentifier: effectivePageLayoutTabUniversalIdentifier,
|
||||
flatEntityMaps:
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps.flatPageLayoutTabMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(referencedPageLayoutTab)) {
|
||||
validationResult.errors.push({
|
||||
code: PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
message: t`Page layout tab not found`,
|
||||
userFriendlyMessage: msg`Page layout tab not found`,
|
||||
});
|
||||
}
|
||||
|
||||
const gridPositionErrors = this.validateGridPosition({
|
||||
gridPosition: updatedFlatPageLayoutWidget.gridPosition,
|
||||
widgetTitle: updatedFlatPageLayoutWidget.title,
|
||||
@@ -235,6 +248,18 @@ export class FlatPageLayoutWidgetValidatorService {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
private getEffectivePageLayoutTabUniversalIdentifier(
|
||||
widget: Pick<
|
||||
UniversalFlatPageLayoutWidget,
|
||||
'pageLayoutTabUniversalIdentifier' | 'universalOverrides'
|
||||
>,
|
||||
): string {
|
||||
return (
|
||||
widget.universalOverrides?.pageLayoutTabUniversalIdentifier ??
|
||||
widget.pageLayoutTabUniversalIdentifier
|
||||
);
|
||||
}
|
||||
|
||||
private validateGridPosition({
|
||||
gridPosition,
|
||||
widgetTitle,
|
||||
|
||||
Reference in New Issue
Block a user