Migrate page layout services (#16443)

Last step of the page layout migration:
- Migrate services
- Write integration tests
This commit is contained in:
Raphaël Bosi
2025-12-10 14:49:44 +01:00
committed by GitHub
parent 1839f8e946
commit f48adb5f07
157 changed files with 6099 additions and 7586 deletions
@@ -0,0 +1,41 @@
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { type CreatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout.input';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
export type FromCreatePageLayoutInputToFlatPageLayoutToCreateArgs = {
createPageLayoutInput: CreatePageLayoutInput;
workspaceId: string;
workspaceCustomApplicationId: string;
};
export const fromCreatePageLayoutInputToFlatPageLayoutToCreate = ({
createPageLayoutInput: rawCreatePageLayoutInput,
workspaceId,
workspaceCustomApplicationId,
}: FromCreatePageLayoutInputToFlatPageLayoutToCreateArgs): FlatPageLayout => {
const createPageLayoutInput =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawCreatePageLayoutInput,
['name'],
);
const createdAt = new Date().toISOString();
const pageLayoutId = v4();
return {
id: pageLayoutId,
name: createPageLayoutInput.name,
type: createPageLayoutInput.type ?? PageLayoutType.RECORD_PAGE,
objectMetadataId: createPageLayoutInput.objectMetadataId ?? null,
workspaceId,
createdAt,
updatedAt: createdAt,
deletedAt: null,
universalIdentifier: pageLayoutId,
applicationId: workspaceCustomApplicationId,
tabIds: [],
};
};
@@ -0,0 +1,43 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout-maps.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import {
PageLayoutException,
PageLayoutExceptionCode,
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
export type DeletePageLayoutInput = {
id: string;
};
export const fromDeletePageLayoutInputToFlatPageLayoutOrThrow = ({
deletePageLayoutInput: rawDeletePageLayoutInput,
flatPageLayoutMaps,
}: {
deletePageLayoutInput: DeletePageLayoutInput;
flatPageLayoutMaps: FlatPageLayoutMaps;
}): FlatPageLayout => {
const { id: pageLayoutId } = extractAndSanitizeObjectStringFields(
rawDeletePageLayoutInput,
['id'],
);
const existingFlatPageLayoutToDelete = flatPageLayoutMaps.byId[pageLayoutId];
if (!isDefined(existingFlatPageLayoutToDelete)) {
throw new PageLayoutException(
t`Page layout to delete not found`,
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
);
}
return {
...existingFlatPageLayoutToDelete,
deletedAt: new Date().toISOString(),
};
};
@@ -0,0 +1,40 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout-maps.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import {
PageLayoutException,
PageLayoutExceptionCode,
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
export type DestroyPageLayoutInput = {
id: string;
};
export const fromDestroyPageLayoutInputToFlatPageLayoutOrThrow = ({
destroyPageLayoutInput,
flatPageLayoutMaps,
}: {
destroyPageLayoutInput: DestroyPageLayoutInput;
flatPageLayoutMaps: FlatPageLayoutMaps;
}): FlatPageLayout => {
const { id: pageLayoutId } = extractAndSanitizeObjectStringFields(
destroyPageLayoutInput,
['id'],
);
const existingFlatPageLayoutToDestroy = flatPageLayoutMaps.byId[pageLayoutId];
if (!isDefined(existingFlatPageLayoutToDestroy)) {
throw new PageLayoutException(
t`Page layout to destroy not found`,
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
);
}
return existingFlatPageLayoutToDestroy;
};
@@ -0,0 +1,50 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout-maps.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import {
PageLayoutException,
PageLayoutExceptionCode,
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
export type RestorePageLayoutInput = {
id: string;
};
export const fromRestorePageLayoutInputToFlatPageLayoutOrThrow = ({
restorePageLayoutInput,
flatPageLayoutMaps,
}: {
restorePageLayoutInput: RestorePageLayoutInput;
flatPageLayoutMaps: FlatPageLayoutMaps;
}): FlatPageLayout => {
const { id: pageLayoutId } = extractAndSanitizeObjectStringFields(
restorePageLayoutInput,
['id'],
);
const existingFlatPageLayoutToRestore = flatPageLayoutMaps.byId[pageLayoutId];
if (!isDefined(existingFlatPageLayoutToRestore)) {
throw new PageLayoutException(
t`Page layout to restore not found`,
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
);
}
if (!isDefined(existingFlatPageLayoutToRestore.deletedAt)) {
throw new PageLayoutException(
t`Page layout is not deleted and cannot be restored`,
PageLayoutExceptionCode.INVALID_PAGE_LAYOUT_DATA,
);
}
return {
...existingFlatPageLayoutToRestore,
deletedAt: null,
};
};
@@ -0,0 +1,54 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { FLAT_PAGE_LAYOUT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout/constants/flat-page-layout-editable-properties.constant';
import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout-maps.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { type UpdatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout.input';
import {
PageLayoutException,
PageLayoutExceptionCode,
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export type UpdatePageLayoutInputWithId = {
id: string;
update: UpdatePageLayoutInput;
};
export const fromUpdatePageLayoutInputToFlatPageLayoutToUpdateOrThrow = ({
updatePageLayoutInput: rawUpdatePageLayoutInput,
flatPageLayoutMaps,
}: {
updatePageLayoutInput: UpdatePageLayoutInputWithId;
flatPageLayoutMaps: FlatPageLayoutMaps;
}): FlatPageLayout => {
const { id: pageLayoutToUpdateId } = extractAndSanitizeObjectStringFields(
rawUpdatePageLayoutInput,
['id'],
);
const existingFlatPageLayoutToUpdate =
flatPageLayoutMaps.byId[pageLayoutToUpdateId];
if (!isDefined(existingFlatPageLayoutToUpdate)) {
throw new PageLayoutException(
t`Page layout to update not found`,
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
);
}
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
rawUpdatePageLayoutInput.update,
FLAT_PAGE_LAYOUT_EDITABLE_PROPERTIES,
);
return mergeUpdateInExistingRecord({
existing: existingFlatPageLayoutToUpdate,
properties: FLAT_PAGE_LAYOUT_EDITABLE_PROPERTIES,
update: updatedEditableFieldProperties,
});
};
@@ -0,0 +1,53 @@
import { isDefined } from 'twenty-shared/utils';
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 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';
export type FlatPageLayoutTabWithWidgets = FlatPageLayoutTab & {
widgets: FlatPageLayoutWidget[];
};
export type FlatPageLayoutWithTabsAndWidgets = FlatPageLayout & {
tabs: FlatPageLayoutTabWithWidgets[];
};
export const reconstructFlatPageLayoutWithTabsAndWidgets = ({
layout,
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
}: {
layout: FlatPageLayout;
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
}): FlatPageLayoutWithTabsAndWidgets => {
const tabs = Object.values(flatPageLayoutTabMaps.byId)
.filter(isDefined)
.filter(
(tab) => tab.pageLayoutId === layout.id && !isDefined(tab.deletedAt),
)
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
const tabsWithWidgets: FlatPageLayoutTabWithWidgets[] = tabs.map((tab) => {
const widgets = Object.values(flatPageLayoutWidgetMaps.byId)
.filter(isDefined)
.filter(
(widget) =>
widget.pageLayoutTabId === tab.id && !isDefined(widget.deletedAt),
);
return {
...tab,
widgets,
widgetIds: widgets.map((widget) => widget.id),
};
});
return {
...layout,
tabs: tabsWithWidgets,
tabIds: tabsWithWidgets.map((tab) => tab.id),
};
};