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
co-authored by Weiko
parent 1e908e5f0f
commit 16e145b036
25 changed files with 415 additions and 132 deletions
@@ -36,6 +36,6 @@ export const fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget = ({
createdAt: now,
updatedAt: now,
deletedAt: null,
overrides: null,
universalOverrides: null,
};
};
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should match snapshot 1`] = `
{
@@ -195,14 +195,14 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"deletedAt",
"conditionalDisplay",
"isActive",
"overrides",
"universalOverrides",
],
"propertiesToStringify": [
"gridPosition",
"position",
"universalConfiguration",
"conditionalDisplay",
"overrides",
"universalOverrides",
],
},
"permissionFlag": {
@@ -921,6 +921,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
toCompare: false,
toStringify: false,
universalProperty: 'pageLayoutTabUniversalIdentifier',
isOverridable: true,
},
conditionalDisplay: {
toCompare: true,
@@ -937,7 +938,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
overrides: {
toCompare: true,
toStringify: true,
universalProperty: undefined,
universalProperty: 'universalOverrides',
},
},
pageLayoutTab: {
@@ -36,6 +36,7 @@ export const splitEntitiesByResetStrategy = <
...entity,
isActive: true as const,
overrides: null,
...('universalOverrides' in entity ? { universalOverrides: null } : {}),
updatedAt: now,
});
}
@@ -8,4 +8,5 @@ export const FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES = [
'position',
'configuration',
'conditionalDisplay',
'pageLayoutTabId',
] as const satisfies MetadataEntityPropertyName<'pageLayoutWidget'>[];
@@ -75,6 +75,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({
applicationUniversalIdentifier: flatApplication.universalIdentifier,
conditionalDisplay: null,
overrides: null,
universalOverrides: null,
universalConfiguration:
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
configuration: createPageLayoutWidgetInput.configuration,
@@ -7,6 +7,7 @@ import {
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
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 FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
type FromPageLayoutWidgetEntityToFlatPageLayoutWidgetArgs =
@@ -81,6 +82,18 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({
viewUniversalIdentifierById,
});
const pageLayoutTabUniversalIdentifierById = Object.fromEntries(
pageLayoutTabIdToUniversalIdentifierMap.entries(),
);
const universalOverrides = isDefined(pageLayoutWidgetEntity.overrides)
? fromPageLayoutWidgetOverridesToUniversalOverrides({
overrides: pageLayoutWidgetEntity.overrides,
pageLayoutTabUniversalIdentifierById,
shouldThrowOnMissingIdentifier: false,
})
: null;
return {
...pageLayoutWidgetEntityWithoutRelations,
createdAt: pageLayoutWidgetEntity.createdAt.toISOString(),
@@ -93,5 +106,6 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({
pageLayoutTabUniversalIdentifier,
objectMetadataUniversalIdentifier,
universalConfiguration: configurationWithUniversalIdentifiers,
universalOverrides,
};
};
@@ -0,0 +1,54 @@
import { type FormatRecordSerializedRelationProperties } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type PageLayoutWidgetOverrides } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
type UniversalPageLayoutWidgetOverrides =
FormatRecordSerializedRelationProperties<PageLayoutWidgetOverrides>;
export const fromPageLayoutWidgetOverridesToUniversalOverrides = ({
overrides,
pageLayoutTabUniversalIdentifierById,
shouldThrowOnMissingIdentifier = true,
}: {
overrides: PageLayoutWidgetOverrides;
pageLayoutTabUniversalIdentifierById: Partial<Record<string, string>>;
shouldThrowOnMissingIdentifier?: boolean;
}): UniversalPageLayoutWidgetOverrides => {
const { pageLayoutTabId, ...scalarOverrides } = overrides;
if (!isDefined(pageLayoutTabId)) {
return {
...scalarOverrides,
...(pageLayoutTabId === null
? { pageLayoutTabUniversalIdentifier: null }
: {}),
};
}
const pageLayoutTabUniversalIdentifier =
pageLayoutTabUniversalIdentifierById[pageLayoutTabId];
if (!isDefined(pageLayoutTabUniversalIdentifier)) {
if (shouldThrowOnMissingIdentifier) {
throw new FlatEntityMapsException(
`PageLayoutTab universal identifier not found for id: ${pageLayoutTabId}`,
FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND,
);
}
return {
...scalarOverrides,
pageLayoutTabUniversalIdentifier: null,
};
}
return {
...scalarOverrides,
pageLayoutTabUniversalIdentifier,
};
};
@@ -11,6 +11,7 @@ import { FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES } from 'src/engine/metadata
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
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 UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input';
import {
PageLayoutWidgetException,
@@ -35,6 +36,7 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro
flatFrontComponentMaps,
flatViewFieldGroupMaps,
flatViewMaps,
flatPageLayoutTabMaps,
callerApplicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
}: {
@@ -49,6 +51,7 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro
| 'flatFrontComponentMaps'
| 'flatViewFieldGroupMaps'
| 'flatViewMaps'
| 'flatPageLayoutTabMaps'
>): FlatPageLayoutWidget => {
const { id: pageLayoutWidgetToUpdateId } =
extractAndSanitizeObjectStringFields(rawUpdatePageLayoutWidgetInput, [
@@ -103,7 +106,21 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro
update: updatedEditableProperties,
}),
overrides,
};
} as FlatPageLayoutWidget;
if (updatedEditableProperties.pageLayoutTabId !== undefined) {
const { pageLayoutTabUniversalIdentifier } =
resolveEntityRelationUniversalIdentifiers({
metadataName: 'pageLayoutWidget',
foreignKeyValues: {
pageLayoutTabId: flatPageLayoutWidgetToUpdate.pageLayoutTabId,
},
flatEntityMaps: { flatPageLayoutTabMaps },
});
flatPageLayoutWidgetToUpdate.pageLayoutTabUniversalIdentifier =
pageLayoutTabUniversalIdentifier;
}
if (updatedEditableProperties.objectMetadataId !== undefined) {
const { objectMetadataUniversalIdentifier } =
@@ -133,5 +150,16 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro
});
}
if (isDefined(overrides)) {
flatPageLayoutWidgetToUpdate.universalOverrides =
fromPageLayoutWidgetOverridesToUniversalOverrides({
overrides,
pageLayoutTabUniversalIdentifierById:
flatPageLayoutTabMaps.universalIdentifierById,
});
} else {
flatPageLayoutWidgetToUpdate.universalOverrides = null;
}
return flatPageLayoutWidgetToUpdate;
};
@@ -5,6 +5,7 @@ import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-la
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { resolveOverridableEntityProperty } from 'src/engine/metadata-modules/utils/resolve-overridable-entity-property.util';
export type FlatPageLayoutTabWithWidgets = FlatPageLayoutTab & {
widgets: FlatPageLayoutWidget[];
@@ -14,6 +15,9 @@ export type FlatPageLayoutWithTabsAndWidgets = FlatPageLayout & {
tabs: FlatPageLayoutTabWithWidgets[];
};
const getResolvedPageLayoutTabId = (widget: FlatPageLayoutWidget): string =>
resolveOverridableEntityProperty(widget, 'pageLayoutTabId');
export const reconstructFlatPageLayoutWithTabsAndWidgets = ({
layout,
flatPageLayoutTabMaps,
@@ -33,7 +37,10 @@ export const reconstructFlatPageLayoutWithTabsAndWidgets = ({
flatPageLayoutWidgetMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((widget) => widget.pageLayoutTabId === tab.id && widget.isActive);
.filter(
(widget) =>
getResolvedPageLayoutTabId(widget) === tab.id && widget.isActive,
);
return {
...tab,
@@ -22,6 +22,11 @@ import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/pa
@InputType()
export class UpdatePageLayoutWidgetInput {
@Field(() => UUIDScalarType, { nullable: true })
@IsUUID()
@IsOptional()
pageLayoutTabId?: string;
@Field({ nullable: true })
@IsString()
@IsOptional()
@@ -4,6 +4,7 @@ import {
PageLayoutWidgetConditionalDisplay,
PageLayoutWidgetPosition,
type GridPosition,
type SerializedRelation,
} from 'twenty-shared/types';
import {
Column,
@@ -30,6 +31,7 @@ export type PageLayoutWidgetOverrides = {
title?: string;
position?: PageLayoutWidgetPosition | null;
conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null;
pageLayoutTabId?: SerializedRelation;
};
@Entity({ name: 'pageLayoutWidget', schema: 'core' })
@@ -18,6 +18,7 @@ import {
import { CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
import { UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input';
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
import { resolveOverridableEntityProperty } from 'src/engine/metadata-modules/utils/resolve-overridable-entity-property.util';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import {
PageLayoutWidgetException,
@@ -162,8 +163,8 @@ export class PageLayoutWidgetService {
.filter(isDefined)
.filter(
(widget) =>
widget.pageLayoutTabId === pageLayoutTabId &&
!isDefined(widget.deletedAt),
resolveOverridableEntityProperty(widget, 'pageLayoutTabId') ===
pageLayoutTabId && !isDefined(widget.deletedAt),
)
.sort(
(widgetA, widgetB) =>
@@ -317,6 +318,7 @@ export class PageLayoutWidgetService {
flatFrontComponentMaps: existingFlatFrontComponentMaps,
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
flatViewMaps: existingFlatViewMaps,
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
},
{ workspaceCustomFlatApplication },
] = await Promise.all([
@@ -329,6 +331,7 @@ export class PageLayoutWidgetService {
'flatFrontComponentMaps',
'flatViewFieldGroupMaps',
'flatViewMaps',
'flatPageLayoutTabMaps',
],
},
),
@@ -368,6 +371,7 @@ export class PageLayoutWidgetService {
flatFrontComponentMaps: existingFlatFrontComponentMaps,
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
flatViewMaps: existingFlatViewMaps,
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
callerApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
workspaceCustomApplicationUniversalIdentifier:
@@ -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,
@@ -1,7 +1,7 @@
import {
type GridPosition,
type PageLayoutWidgetConditionalDisplay,
type PageLayoutWidgetPosition,
type GridPosition,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -110,5 +110,6 @@ export const createStandardPageLayoutWidgetFlatMetadata = ({
deletedAt: null,
conditionalDisplay: conditionalDisplay ?? null,
overrides: null,
universalOverrides: null,
};
};
@@ -34,6 +34,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
pageLayoutTab: {},
pageLayoutWidget: {
configuration: 'configuration',
overrides: 'overrides',
},
commandMenuItem: {},
navigationMenuItem: {},
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
@@ -11,6 +12,7 @@ import {
UniversalCreatePageLayoutWidgetAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/types/workspace-migration-page-layout-widget-action.type';
import { fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-configuration-to-flat-page-layout-widget-configuration.util';
import { fromUniversalOverridesToPageLayoutWidgetOverrides } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-overrides-to-page-layout-widget-overrides.util';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
@@ -47,6 +49,13 @@ export class CreatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati
flatViewFieldGroupMaps: allFlatEntityMaps.flatViewFieldGroupMaps,
});
const overrides = isDefined(action.flatEntity.universalOverrides)
? fromUniversalOverridesToPageLayoutWidgetOverrides({
universalOverrides: action.flatEntity.universalOverrides,
flatPageLayoutTabMaps: allFlatEntityMaps.flatPageLayoutTabMaps,
})
: null;
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'pageLayoutWidget',
@@ -57,6 +66,7 @@ export class CreatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati
flatEntity: {
...action.flatEntity,
configuration,
overrides,
pageLayoutTabId,
objectMetadataId,
applicationId: flatApplication.id,
@@ -10,6 +10,7 @@ import {
UniversalUpdatePageLayoutWidgetAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/types/workspace-migration-page-layout-widget-action.type';
import { fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-configuration-to-flat-page-layout-widget-configuration.util';
import { fromUniversalOverridesToPageLayoutWidgetOverrides } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-overrides-to-page-layout-widget-overrides.util';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
@@ -34,14 +35,17 @@ export class UpdatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati
universalIdentifier: action.universalIdentifier,
});
const { universalConfiguration, ...updateWithResolvedForeignKeys } =
resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'pageLayoutWidget',
universalUpdate: action.update,
allFlatEntityMaps,
});
const {
universalConfiguration,
universalOverrides,
...updateWithResolvedForeignKeys
} = resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'pageLayoutWidget',
universalUpdate: action.update,
allFlatEntityMaps,
});
const update =
const updateWithConfiguration =
universalConfiguration === undefined
? updateWithResolvedForeignKeys
: {
@@ -58,6 +62,19 @@ export class UpdatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati
}),
};
const update =
universalOverrides === undefined
? updateWithConfiguration
: universalOverrides === null
? { ...updateWithConfiguration, overrides: null }
: {
...updateWithConfiguration,
overrides: fromUniversalOverridesToPageLayoutWidgetOverrides({
universalOverrides,
flatPageLayoutTabMaps: allFlatEntityMaps.flatPageLayoutTabMaps,
}),
};
return {
type: 'update',
metadataName: 'pageLayoutWidget',
@@ -0,0 +1,39 @@
import { type FormatRecordSerializedRelationProperties } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { type PageLayoutWidgetOverrides } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
type UniversalPageLayoutWidgetOverrides =
FormatRecordSerializedRelationProperties<PageLayoutWidgetOverrides>;
export const fromUniversalOverridesToPageLayoutWidgetOverrides = ({
universalOverrides,
flatPageLayoutTabMaps,
}: {
universalOverrides: UniversalPageLayoutWidgetOverrides;
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
}): PageLayoutWidgetOverrides => {
const { pageLayoutTabUniversalIdentifier, ...scalarOverrides } =
universalOverrides;
if (!isDefined(pageLayoutTabUniversalIdentifier)) {
return {
...scalarOverrides,
};
}
const flatPageLayoutTab =
findFlatEntityByUniversalIdentifierOrThrow<FlatPageLayoutTab>({
flatEntityMaps: flatPageLayoutTabMaps,
universalIdentifier: pageLayoutTabUniversalIdentifier,
});
return {
...scalarOverrides,
pageLayoutTabId:
flatPageLayoutTab.id as PageLayoutWidgetOverrides['pageLayoutTabId'],
};
};