Fix moving a widget to another tab (#19450)

https://github.com/user-attachments/assets/aac81e79-7f2f-4a34-bf68-c76061086821

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Baptiste Devessier
2026-04-09 15:59:04 +02:00
committed by GitHub
parent 1e908e5f0f
commit 16e145b036
25 changed files with 415 additions and 132 deletions
@@ -116,6 +116,7 @@ export class PageLayoutResetService {
const widgetToUpdate: FlatPageLayoutWidget = {
...widget,
overrides: null,
universalOverrides: null,
updatedAt: now,
};
@@ -547,15 +548,10 @@ export class PageLayoutResetService {
now,
});
const viewFieldsToReset = fieldsToReset.map((field) => ({
...field,
universalOverrides: null,
}));
return {
viewFieldGroupsToUpdate: groupsToReset,
viewFieldGroupsToDelete: groupsToDelete,
viewFieldsToUpdate: viewFieldsToReset,
viewFieldsToUpdate: fieldsToReset,
viewFieldsToDelete: fieldsToDelete,
};
}
@@ -10,6 +10,7 @@ import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
import { splitEntitiesByRemovalStrategy } from 'src/engine/metadata-modules/flat-entity/utils/split-entities-by-removal-strategy.util';
import { FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-tab/constants/flat-page-layout-tab-editable-properties.constant';
import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
@@ -18,6 +19,7 @@ import { FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES } from 'src/engine/metadata
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { buildFlatPageLayoutWidgetCommonProperties } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/build-flat-page-layout-widget-common-properties.util';
import { fromPageLayoutWidgetConfigurationToUniversalConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util';
import { fromPageLayoutWidgetOverridesToUniversalOverrides } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-overrides-to-universal-overrides.util';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { reconstructFlatPageLayoutWithTabsAndWidgets } from 'src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util';
import { UpdatePageLayoutTabWithWidgetsInput } from 'src/engine/metadata-modules/page-layout-tab/dtos/inputs/update-page-layout-tab-with-widgets.input';
@@ -468,11 +470,16 @@ export class PageLayoutUpdateService {
const allWidgetsToUpdate: FlatPageLayoutWidget[] = [];
const allWidgetsToDelete: FlatPageLayoutWidget[] = [];
const widgetIdsAcrossAllTabs = new Set(
tabs.flatMap((tab) => tab.widgets.map((widget) => widget.id)),
);
for (const tabInput of tabs) {
const { widgetsToCreate, widgetsToUpdate, widgetsToDelete } =
this.computeWidgetOperationsForTab({
tabId: tabInput.id,
widgets: tabInput.widgets,
widgetIdsAcrossAllTabs,
flatPageLayoutWidgetMaps,
flatPageLayoutTabMaps,
flatObjectMetadataMaps,
@@ -500,6 +507,7 @@ export class PageLayoutUpdateService {
private computeWidgetOperationsForTab({
tabId,
widgets,
widgetIdsAcrossAllTabs,
flatPageLayoutWidgetMaps,
flatPageLayoutTabMaps,
flatObjectMetadataMaps,
@@ -513,6 +521,7 @@ export class PageLayoutUpdateService {
}: {
tabId: string;
widgets: UpdatePageLayoutWidgetWithIdInput[];
widgetIdsAcrossAllTabs: Set<string>;
workspaceId: string;
workspaceCustomApplicationId: string;
workspaceCustomApplicationUniversalIdentifier: string;
@@ -538,11 +547,15 @@ export class PageLayoutUpdateService {
});
}
const existingWidgets = Object.values(
flatPageLayoutWidgetMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((widget) => widget.pageLayoutTabId === tabId);
const widgetIdsInCurrentTabInput = new Set(
widgets.map((widget) => widget.id),
);
const existingWidgets = this.findWidgetsInTabOrMovingToTab({
tabId,
widgetIdsInCurrentTabInput,
flatPageLayoutWidgetMaps,
});
const resolvedExistingWidgets = existingWidgets.map(
resolveFlatEntityOverridableProperties,
@@ -587,6 +600,7 @@ export class PageLayoutUpdateService {
workspaceCustomApplicationUniversalIdentifier,
conditionalDisplay: null,
overrides: null,
universalOverrides: null,
isActive: true,
universalConfiguration:
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
@@ -604,120 +618,45 @@ export class PageLayoutUpdateService {
);
const widgetsToUpdate: FlatPageLayoutWidget[] = entitiesToUpdate.map(
(widgetInput) => {
const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: widgetInput.id,
flatEntityMaps: flatPageLayoutWidgetMaps,
});
const shouldOverride = isCallerOverridingEntity({
callerApplicationUniversalIdentifier:
workspaceCustomApplicationUniversalIdentifier,
entityApplicationUniversalIdentifier:
existingWidget.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
});
const commonProperties = buildFlatPageLayoutWidgetCommonProperties({
(widgetInput) =>
this.buildUpdatedFlatPageLayoutWidget({
widgetInput,
flatPageLayoutWidgetMaps,
flatPageLayoutTabMaps,
flatObjectMetadataMaps,
});
const updatedConfiguration = widgetInput.configuration ?? null;
const { overrides, updatedEditableProperties } =
sanitizeOverridableEntityInput({
metadataName: 'pageLayoutWidget',
existingFlatEntity: existingWidget,
updatedEditableProperties: {
...commonProperties,
configuration: updatedConfiguration,
},
shouldOverride,
});
return {
...existingWidget,
...updatedEditableProperties,
overrides,
updatedAt: now.toISOString(),
...(isDefined(updatedConfiguration) && {
universalConfiguration:
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
configuration: updatedConfiguration,
fieldMetadataUniversalIdentifierById:
flatFieldMetadataMaps.universalIdentifierById,
frontComponentUniversalIdentifierById:
flatFrontComponentMaps.universalIdentifierById,
viewFieldGroupUniversalIdentifierById:
flatViewFieldGroupMaps.universalIdentifierById,
viewUniversalIdentifierById:
flatViewMaps.universalIdentifierById,
}),
}),
};
},
flatFieldMetadataMaps,
flatFrontComponentMaps,
flatViewFieldGroupMaps,
flatViewMaps,
workspaceCustomApplicationUniversalIdentifier,
now,
}),
);
const widgetsToRestoreAndUpdate: FlatPageLayoutWidget[] =
entitiesToRestoreAndUpdate.map((widgetInput) => {
const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: widgetInput.id,
flatEntityMaps: flatPageLayoutWidgetMaps,
});
const shouldOverride = isCallerOverridingEntity({
callerApplicationUniversalIdentifier:
workspaceCustomApplicationUniversalIdentifier,
entityApplicationUniversalIdentifier:
existingWidget.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
});
const commonProperties = buildFlatPageLayoutWidgetCommonProperties({
entitiesToRestoreAndUpdate.map((widgetInput) => ({
...this.buildUpdatedFlatPageLayoutWidget({
widgetInput,
flatPageLayoutWidgetMaps,
flatPageLayoutTabMaps,
flatObjectMetadataMaps,
});
flatFieldMetadataMaps,
flatFrontComponentMaps,
flatViewFieldGroupMaps,
flatViewMaps,
workspaceCustomApplicationUniversalIdentifier,
now,
}),
isActive: true,
}));
const restoredConfiguration = widgetInput.configuration ?? null;
const { overrides, updatedEditableProperties } =
sanitizeOverridableEntityInput({
metadataName: 'pageLayoutWidget',
existingFlatEntity: existingWidget,
updatedEditableProperties: {
...commonProperties,
configuration: restoredConfiguration,
},
shouldOverride,
});
return {
...existingWidget,
...updatedEditableProperties,
overrides,
isActive: true,
updatedAt: now.toISOString(),
...(isDefined(restoredConfiguration) && {
universalConfiguration:
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
configuration: restoredConfiguration,
fieldMetadataUniversalIdentifierById:
flatFieldMetadataMaps.universalIdentifierById,
frontComponentUniversalIdentifierById:
flatFrontComponentMaps.universalIdentifierById,
viewFieldGroupUniversalIdentifierById:
flatViewFieldGroupMaps.universalIdentifierById,
viewUniversalIdentifierById:
flatViewMaps.universalIdentifierById,
}),
}),
};
const widgetIdsToRemoveExcludingMovedToOtherTabs =
this.excludeWidgetsMovedToOtherTabs({
idsToRemove,
widgetIdsAcrossAllTabs,
});
const widgetsToRemove = idsToRemove
const widgetsToRemove = widgetIdsToRemoveExcludingMovedToOtherTabs
.map((widgetId) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: widgetId,
@@ -743,6 +682,162 @@ export class PageLayoutUpdateService {
};
}
private buildUpdatedFlatPageLayoutWidget({
widgetInput,
flatPageLayoutWidgetMaps,
flatPageLayoutTabMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatFrontComponentMaps,
flatViewFieldGroupMaps,
flatViewMaps,
workspaceCustomApplicationUniversalIdentifier,
now,
}: {
widgetInput: UpdatePageLayoutWidgetWithIdInput;
workspaceCustomApplicationUniversalIdentifier: string;
now: Date;
} & Pick<
AllFlatEntityMaps,
| 'flatObjectMetadataMaps'
| 'flatFieldMetadataMaps'
| 'flatFrontComponentMaps'
| 'flatViewFieldGroupMaps'
| 'flatViewMaps'
| 'flatPageLayoutTabMaps'
| 'flatPageLayoutWidgetMaps'
>): FlatPageLayoutWidget {
const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: widgetInput.id,
flatEntityMaps: flatPageLayoutWidgetMaps,
});
const shouldOverride = isCallerOverridingEntity({
callerApplicationUniversalIdentifier:
workspaceCustomApplicationUniversalIdentifier,
entityApplicationUniversalIdentifier:
existingWidget.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
});
const configuration = widgetInput.configuration ?? null;
const editableProperties: Partial<FlatPageLayoutWidget> = {
title: widgetInput.title,
type: widgetInput.type,
objectMetadataId: widgetInput.objectMetadataId ?? null,
gridPosition: widgetInput.gridPosition,
position: widgetInput.position ?? null,
configuration,
pageLayoutTabId: widgetInput.pageLayoutTabId,
};
if (widgetInput.conditionalDisplay !== undefined) {
editableProperties.conditionalDisplay =
widgetInput.conditionalDisplay ?? null;
}
const { overrides, updatedEditableProperties } =
sanitizeOverridableEntityInput({
metadataName: 'pageLayoutWidget',
existingFlatEntity: existingWidget,
updatedEditableProperties: editableProperties,
shouldOverride,
});
const updatedWidget: FlatPageLayoutWidget = {
...existingWidget,
...updatedEditableProperties,
overrides,
updatedAt: now.toISOString(),
};
if (updatedEditableProperties.pageLayoutTabId !== undefined) {
const { pageLayoutTabUniversalIdentifier } =
resolveEntityRelationUniversalIdentifiers({
metadataName: 'pageLayoutWidget',
foreignKeyValues: {
pageLayoutTabId: updatedWidget.pageLayoutTabId,
},
flatEntityMaps: { flatPageLayoutTabMaps },
});
updatedWidget.pageLayoutTabUniversalIdentifier =
pageLayoutTabUniversalIdentifier;
}
if (updatedEditableProperties.objectMetadataId !== undefined) {
const { objectMetadataUniversalIdentifier } =
resolveEntityRelationUniversalIdentifiers({
metadataName: 'pageLayoutWidget',
foreignKeyValues: {
objectMetadataId: updatedWidget.objectMetadataId,
},
flatEntityMaps: { flatObjectMetadataMaps },
});
updatedWidget.objectMetadataUniversalIdentifier =
objectMetadataUniversalIdentifier;
}
if (isDefined(overrides)) {
updatedWidget.universalOverrides =
fromPageLayoutWidgetOverridesToUniversalOverrides({
overrides,
pageLayoutTabUniversalIdentifierById:
flatPageLayoutTabMaps.universalIdentifierById,
});
} else {
updatedWidget.universalOverrides = null;
}
if (isDefined(configuration)) {
updatedWidget.universalConfiguration =
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
configuration,
fieldMetadataUniversalIdentifierById:
flatFieldMetadataMaps.universalIdentifierById,
frontComponentUniversalIdentifierById:
flatFrontComponentMaps.universalIdentifierById,
viewFieldGroupUniversalIdentifierById:
flatViewFieldGroupMaps.universalIdentifierById,
viewUniversalIdentifierById: flatViewMaps.universalIdentifierById,
});
}
return updatedWidget;
}
private findWidgetsInTabOrMovingToTab({
tabId,
widgetIdsInCurrentTabInput,
flatPageLayoutWidgetMaps,
}: {
tabId: string;
widgetIdsInCurrentTabInput: Set<string>;
flatPageLayoutWidgetMaps: AllFlatEntityMaps['flatPageLayoutWidgetMaps'];
}): FlatPageLayoutWidget[] {
return Object.values(flatPageLayoutWidgetMaps.byUniversalIdentifier)
.filter(isDefined)
.filter(
(widget) =>
widget.pageLayoutTabId === tabId ||
widgetIdsInCurrentTabInput.has(widget.id),
);
}
private excludeWidgetsMovedToOtherTabs({
idsToRemove,
widgetIdsAcrossAllTabs,
}: {
idsToRemove: string[];
widgetIdsAcrossAllTabs: Set<string>;
}): string[] {
return idsToRemove.filter(
(widgetId) => !widgetIdsAcrossAllTabs.has(widgetId),
);
}
private validateChartFieldReferences({
widgetInput,
flatFieldMetadataMaps,