Migrate page layout services (#16443)
Last step of the page layout migration: - Migrate services - Write integration tests
This commit is contained in:
+6
-3
@@ -5,10 +5,10 @@ import { FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/meta
|
||||
import { FLAT_AGENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-agent/constants/flat-agent-editable-properties.constant';
|
||||
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant';
|
||||
import { FLAT_PAGE_LAYOUT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout/constants/flat-page-layout-editable-properties.constant';
|
||||
import { FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant';
|
||||
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 { FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant';
|
||||
import { FLAT_PAGE_LAYOUT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout/constants/flat-page-layout-editable-properties.constant';
|
||||
import { FLAT_ROLE_TARGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-role-target/constants/flat-role-target-editable-properties.constant';
|
||||
import { FLAT_ROLE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant';
|
||||
import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant';
|
||||
@@ -115,7 +115,7 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
|
||||
],
|
||||
},
|
||||
pageLayout: {
|
||||
propertiesToCompare: [...FLAT_PAGE_LAYOUT_EDITABLE_PROPERTIES],
|
||||
propertiesToCompare: [...FLAT_PAGE_LAYOUT_EDITABLE_PROPERTIES, 'deletedAt'],
|
||||
propertiesToStringify: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
@@ -126,7 +126,10 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
|
||||
propertiesToStringify: ['gridPosition', 'configuration'],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
propertiesToCompare: [...FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES],
|
||||
propertiesToCompare: [
|
||||
...FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES,
|
||||
'deletedAt',
|
||||
],
|
||||
propertiesToStringify: [],
|
||||
},
|
||||
} as const satisfies {
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { type CreatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout-tab.input';
|
||||
|
||||
export type FromCreatePageLayoutTabInputToFlatPageLayoutTabToCreateArgs = {
|
||||
createPageLayoutTabInput: CreatePageLayoutTabInput;
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
};
|
||||
|
||||
export const fromCreatePageLayoutTabInputToFlatPageLayoutTabToCreate = ({
|
||||
createPageLayoutTabInput: rawCreatePageLayoutTabInput,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
}: FromCreatePageLayoutTabInputToFlatPageLayoutTabToCreateArgs): FlatPageLayoutTab => {
|
||||
const createPageLayoutTabInput =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
rawCreatePageLayoutTabInput,
|
||||
['title'],
|
||||
);
|
||||
|
||||
const createdAt = new Date().toISOString();
|
||||
const pageLayoutTabId = v4();
|
||||
|
||||
return {
|
||||
id: pageLayoutTabId,
|
||||
title: createPageLayoutTabInput.title,
|
||||
position: createPageLayoutTabInput.position ?? 0,
|
||||
pageLayoutId: createPageLayoutTabInput.pageLayoutId,
|
||||
workspaceId,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
universalIdentifier: pageLayoutTabId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
widgetIds: [],
|
||||
};
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
extractAndSanitizeObjectStringFields,
|
||||
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 {
|
||||
PageLayoutTabException,
|
||||
PageLayoutTabExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-tab.exception';
|
||||
|
||||
export type DeletePageLayoutTabInput = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const fromDeletePageLayoutTabInputToFlatPageLayoutTabOrThrow = ({
|
||||
deletePageLayoutTabInput: rawDeletePageLayoutTabInput,
|
||||
flatPageLayoutTabMaps,
|
||||
}: {
|
||||
deletePageLayoutTabInput: DeletePageLayoutTabInput;
|
||||
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
|
||||
}): FlatPageLayoutTab => {
|
||||
const { id: pageLayoutTabId } = extractAndSanitizeObjectStringFields(
|
||||
rawDeletePageLayoutTabInput,
|
||||
['id'],
|
||||
);
|
||||
|
||||
const existingFlatPageLayoutTabToDelete =
|
||||
flatPageLayoutTabMaps.byId[pageLayoutTabId];
|
||||
|
||||
if (!isDefined(existingFlatPageLayoutTabToDelete)) {
|
||||
throw new PageLayoutTabException(
|
||||
t`Page layout tab to delete not found`,
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...existingFlatPageLayoutTabToDelete,
|
||||
deletedAt: new Date().toISOString(),
|
||||
};
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
extractAndSanitizeObjectStringFields,
|
||||
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 {
|
||||
PageLayoutTabException,
|
||||
PageLayoutTabExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-tab.exception';
|
||||
|
||||
export type DestroyPageLayoutTabInput = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const fromDestroyPageLayoutTabInputToFlatPageLayoutTabOrThrow = ({
|
||||
destroyPageLayoutTabInput,
|
||||
flatPageLayoutTabMaps,
|
||||
}: {
|
||||
destroyPageLayoutTabInput: DestroyPageLayoutTabInput;
|
||||
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
|
||||
}): FlatPageLayoutTab => {
|
||||
const { id: pageLayoutTabId } = extractAndSanitizeObjectStringFields(
|
||||
destroyPageLayoutTabInput,
|
||||
['id'],
|
||||
);
|
||||
|
||||
const existingFlatPageLayoutTabToDestroy =
|
||||
flatPageLayoutTabMaps.byId[pageLayoutTabId];
|
||||
|
||||
if (!isDefined(existingFlatPageLayoutTabToDestroy)) {
|
||||
throw new PageLayoutTabException(
|
||||
t`Page layout tab to destroy not found`,
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return existingFlatPageLayoutTabToDestroy;
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
extractAndSanitizeObjectStringFields,
|
||||
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 {
|
||||
PageLayoutTabException,
|
||||
PageLayoutTabExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-tab.exception';
|
||||
|
||||
export type RestorePageLayoutTabInput = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const fromRestorePageLayoutTabInputToFlatPageLayoutTabOrThrow = ({
|
||||
restorePageLayoutTabInput,
|
||||
flatPageLayoutTabMaps,
|
||||
}: {
|
||||
restorePageLayoutTabInput: RestorePageLayoutTabInput;
|
||||
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
|
||||
}): FlatPageLayoutTab => {
|
||||
const { id: pageLayoutTabId } = extractAndSanitizeObjectStringFields(
|
||||
restorePageLayoutTabInput,
|
||||
['id'],
|
||||
);
|
||||
|
||||
const existingFlatPageLayoutTabToRestore =
|
||||
flatPageLayoutTabMaps.byId[pageLayoutTabId];
|
||||
|
||||
if (!isDefined(existingFlatPageLayoutTabToRestore)) {
|
||||
throw new PageLayoutTabException(
|
||||
t`Page layout tab to restore not found`,
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(existingFlatPageLayoutTabToRestore.deletedAt)) {
|
||||
throw new PageLayoutTabException(
|
||||
t`Page layout tab is not deleted and cannot be restored`,
|
||||
PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...existingFlatPageLayoutTabToRestore,
|
||||
deletedAt: null,
|
||||
};
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
extractAndSanitizeObjectStringFields,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
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';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { type UpdatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-tab.input';
|
||||
import {
|
||||
PageLayoutTabException,
|
||||
PageLayoutTabExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-tab.exception';
|
||||
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
|
||||
|
||||
export type UpdatePageLayoutTabInputWithId = {
|
||||
id: string;
|
||||
update: UpdatePageLayoutTabInput;
|
||||
};
|
||||
|
||||
export const fromUpdatePageLayoutTabInputToFlatPageLayoutTabToUpdateOrThrow = ({
|
||||
updatePageLayoutTabInput: rawUpdatePageLayoutTabInput,
|
||||
flatPageLayoutTabMaps,
|
||||
}: {
|
||||
updatePageLayoutTabInput: UpdatePageLayoutTabInputWithId;
|
||||
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
|
||||
}): FlatPageLayoutTab => {
|
||||
const { id: pageLayoutTabToUpdateId } = extractAndSanitizeObjectStringFields(
|
||||
rawUpdatePageLayoutTabInput,
|
||||
['id'],
|
||||
);
|
||||
|
||||
const existingFlatPageLayoutTabToUpdate =
|
||||
flatPageLayoutTabMaps.byId[pageLayoutTabToUpdateId];
|
||||
|
||||
if (!isDefined(existingFlatPageLayoutTabToUpdate)) {
|
||||
throw new PageLayoutTabException(
|
||||
t`Page layout tab to update not found`,
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
|
||||
rawUpdatePageLayoutTabInput.update,
|
||||
FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES,
|
||||
);
|
||||
|
||||
return mergeUpdateInExistingRecord({
|
||||
existing: existingFlatPageLayoutTabToUpdate,
|
||||
properties: [...FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES],
|
||||
update: updatedEditableFieldProperties,
|
||||
});
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
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';
|
||||
|
||||
export type FlatPageLayoutTabWithWidgets = FlatPageLayoutTab & {
|
||||
widgets: FlatPageLayoutWidget[];
|
||||
};
|
||||
|
||||
export const reconstructFlatPageLayoutTabWithWidgets = ({
|
||||
tab,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}: {
|
||||
tab: FlatPageLayoutTab;
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
}): FlatPageLayoutTabWithWidgets => {
|
||||
const widgets = Object.values(flatPageLayoutWidgetMaps.byId).filter(
|
||||
(widget): widget is FlatPageLayoutWidget =>
|
||||
isDefined(widget) &&
|
||||
widget.pageLayoutTabId === tab.id &&
|
||||
!isDefined(widget.deletedAt),
|
||||
);
|
||||
|
||||
return {
|
||||
...tab,
|
||||
widgets,
|
||||
widgetIds: widgets.map((widget) => widget.id),
|
||||
};
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
extractAndSanitizeObjectStringFields,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
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 {
|
||||
PageLayoutWidgetException,
|
||||
PageLayoutWidgetExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-widget.exception';
|
||||
|
||||
export type RestorePageLayoutWidgetInput = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const fromRestorePageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow = ({
|
||||
restorePageLayoutWidgetInput,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}: {
|
||||
restorePageLayoutWidgetInput: RestorePageLayoutWidgetInput;
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
}): FlatPageLayoutWidget => {
|
||||
const { id: pageLayoutWidgetId } = extractAndSanitizeObjectStringFields(
|
||||
restorePageLayoutWidgetInput,
|
||||
['id'],
|
||||
);
|
||||
|
||||
const existingFlatPageLayoutWidgetToRestore =
|
||||
flatPageLayoutWidgetMaps.byId[pageLayoutWidgetId];
|
||||
|
||||
if (!isDefined(existingFlatPageLayoutWidgetToRestore)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
t`Page layout widget to restore not found`,
|
||||
PageLayoutWidgetExceptionCode.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(existingFlatPageLayoutWidgetToRestore.deletedAt)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
t`Page layout widget is not deleted and cannot be restored`,
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...existingFlatPageLayoutWidgetToRestore,
|
||||
deletedAt: null,
|
||||
};
|
||||
};
|
||||
+4
-5
@@ -1,7 +1,6 @@
|
||||
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
|
||||
|
||||
export const FLAT_PAGE_LAYOUT_EDITABLE_PROPERTIES: (keyof FlatPageLayout)[] = [
|
||||
'name',
|
||||
'type',
|
||||
'objectMetadataId',
|
||||
];
|
||||
export const FLAT_PAGE_LAYOUT_EDITABLE_PROPERTIES: (keyof Pick<
|
||||
FlatPageLayout,
|
||||
'name' | 'type' | 'objectMetadataId'
|
||||
>)[] = ['name', 'type', 'objectMetadataId'];
|
||||
|
||||
+2
-1
@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { WorkspaceFlatPageLayoutMapCacheService } from 'src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([PageLayoutEntity], 'core')],
|
||||
imports: [TypeOrmModule.forFeature([PageLayoutEntity, PageLayoutTabEntity])],
|
||||
providers: [WorkspaceFlatPageLayoutMapCacheService],
|
||||
exports: [WorkspaceFlatPageLayoutMapCacheService],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './types/flat-page-layout.type';
|
||||
export * from './types/flat-page-layout-maps.type';
|
||||
export * from './constants/flat-page-layout-editable-properties.constant';
|
||||
export * from './utils/transform-page-layout-entity-to-flat-page-layout.util';
|
||||
export * from './services/workspace-flat-page-layout-map-cache.service';
|
||||
export * from './flat-page-layout.module';
|
||||
+41
@@ -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: [],
|
||||
};
|
||||
};
|
||||
+43
@@ -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(),
|
||||
};
|
||||
};
|
||||
+40
@@ -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;
|
||||
};
|
||||
+50
@@ -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,
|
||||
};
|
||||
};
|
||||
+54
@@ -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,
|
||||
});
|
||||
};
|
||||
+53
@@ -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),
|
||||
};
|
||||
};
|
||||
+7
-8
@@ -11,20 +11,19 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { CreatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout.input';
|
||||
import { UpdatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout.input';
|
||||
import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { PageLayoutRestApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/filters/page-layout-rest-api-exception.filter';
|
||||
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout.input';
|
||||
import { UpdatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout.input';
|
||||
import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto';
|
||||
import { PageLayoutRestApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/filters/page-layout-rest-api-exception.filter';
|
||||
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
|
||||
@Controller('rest/metadata/pageLayouts')
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@@ -87,7 +86,7 @@ export class PageLayoutController {
|
||||
async delete(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<PageLayoutEntity> {
|
||||
): Promise<PageLayoutDTO> {
|
||||
const deletedPageLayout = await this.pageLayoutService.delete(
|
||||
id,
|
||||
workspace.id,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { I18nModule } from 'src/engine/core-modules/i18n/i18n.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { FlatPageLayoutTabModule } from 'src/engine/metadata-modules/flat-page-layout-tab/flat-page-layout-tab.module';
|
||||
import { FlatPageLayoutWidgetModule } from 'src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module';
|
||||
import { FlatPageLayoutModule } from 'src/engine/metadata-modules/flat-page-layout/flat-page-layout.module';
|
||||
import { PageLayoutTabController } from 'src/engine/metadata-modules/page-layout/controllers/page-layout-tab.controller';
|
||||
import { PageLayoutWidgetController } from 'src/engine/metadata-modules/page-layout/controllers/page-layout-widget.controller';
|
||||
import { PageLayoutController } from 'src/engine/metadata-modules/page-layout/controllers/page-layout.controller';
|
||||
@@ -22,6 +25,7 @@ import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/servi
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
|
||||
|
||||
@Module({
|
||||
@@ -35,11 +39,14 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
|
||||
TwentyORMModule,
|
||||
PermissionsModule,
|
||||
FeatureFlagModule,
|
||||
I18nModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMigrationV2Module,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
FlatPageLayoutModule,
|
||||
FlatPageLayoutTabModule,
|
||||
FlatPageLayoutWidgetModule,
|
||||
ApplicationModule,
|
||||
],
|
||||
controllers: [
|
||||
PageLayoutController,
|
||||
@@ -54,6 +61,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
|
||||
PageLayoutTabResolver,
|
||||
PageLayoutWidgetResolver,
|
||||
PageLayoutUpdateService,
|
||||
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
exports: [PageLayoutService, PageLayoutTabService, PageLayoutWidgetService],
|
||||
})
|
||||
|
||||
+14
-7
@@ -1,22 +1,29 @@
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import {
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { CreatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout-tab.input';
|
||||
import { UpdatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-tab.input';
|
||||
import { PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-tab.dto';
|
||||
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout/services/page-layout-tab.service';
|
||||
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout-tab.input';
|
||||
import { UpdatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-tab.input';
|
||||
import { PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-tab.dto';
|
||||
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout/services/page-layout-tab.service';
|
||||
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
|
||||
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
|
||||
|
||||
@Resolver(() => PageLayoutTabDTO)
|
||||
@UseInterceptors(WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor)
|
||||
@UseFilters(PageLayoutGraphqlApiExceptionFilter)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
|
||||
+8
-1
@@ -1,4 +1,9 @@
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import {
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
Mutation,
|
||||
@@ -23,8 +28,10 @@ import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout/dto
|
||||
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout/services/page-layout-widget.service';
|
||||
import { injectWidgetConfigurationDiscriminator } from 'src/engine/metadata-modules/page-layout/utils/inject-widget-configuration-discriminator.util';
|
||||
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
|
||||
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
|
||||
|
||||
@Resolver(() => PageLayoutWidgetDTO)
|
||||
@UseInterceptors(WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor)
|
||||
@UseFilters(PageLayoutGraphqlApiExceptionFilter)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
|
||||
+14
-7
@@ -1,10 +1,20 @@
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import {
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout.input';
|
||||
import { UpdatePageLayoutWithTabsInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input';
|
||||
import { UpdatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout.input';
|
||||
@@ -12,13 +22,10 @@ import { PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page
|
||||
import { PageLayoutUpdateService } from 'src/engine/metadata-modules/page-layout/services/page-layout-update.service';
|
||||
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
|
||||
|
||||
@Resolver(() => PageLayoutDTO)
|
||||
@UseInterceptors(WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor)
|
||||
@UseFilters(PageLayoutGraphqlApiExceptionFilter)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
|
||||
-606
@@ -1,606 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity';
|
||||
import { type PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-widget.entity';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout/enums/widget-type.enum';
|
||||
import {
|
||||
generatePageLayoutTabExceptionMessage,
|
||||
PageLayoutTabException,
|
||||
PageLayoutTabExceptionCode,
|
||||
PageLayoutTabExceptionMessageKey,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-tab.exception';
|
||||
import {
|
||||
PageLayoutException,
|
||||
PageLayoutExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
|
||||
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout/services/page-layout-tab.service';
|
||||
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
|
||||
describe('PageLayoutTabService', () => {
|
||||
let pageLayoutTabService: PageLayoutTabService;
|
||||
let pageLayoutTabRepository: Repository<PageLayoutTabEntity>;
|
||||
let pageLayoutService: PageLayoutService;
|
||||
|
||||
const mockPageLayoutTab = {
|
||||
id: 'page-layout-tab-id',
|
||||
title: 'Test Tab',
|
||||
position: 0,
|
||||
pageLayoutId: 'page-layout-id',
|
||||
pageLayout: {} as any,
|
||||
workspaceId: 'workspace-id',
|
||||
workspace: {} as any,
|
||||
widgets: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
application: {} as ApplicationEntity,
|
||||
applicationId: 'application-id',
|
||||
universalIdentifier: 'universal-identifier',
|
||||
} as PageLayoutTabEntity;
|
||||
|
||||
const mockWidget = {
|
||||
id: 'widget-1',
|
||||
title: 'Test Widget',
|
||||
type: WidgetType.VIEW,
|
||||
pageLayoutTabId: 'page-layout-tab-id',
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
configuration: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
} as PageLayoutWidgetEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PageLayoutTabService,
|
||||
{
|
||||
provide: getRepositoryToken(PageLayoutTabEntity),
|
||||
useValue: {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
update: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
restore: jest.fn(),
|
||||
insert: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PageLayoutService,
|
||||
useValue: {
|
||||
findByIdOrThrow: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
pageLayoutTabService =
|
||||
module.get<PageLayoutTabService>(PageLayoutTabService);
|
||||
pageLayoutTabRepository = module.get<Repository<PageLayoutTabEntity>>(
|
||||
getRepositoryToken(PageLayoutTabEntity),
|
||||
);
|
||||
pageLayoutService = module.get<PageLayoutService>(PageLayoutService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(pageLayoutTabService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findByPageLayoutId', () => {
|
||||
it('should return page layout tabs for a page layout id', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutId = 'page-layout-id';
|
||||
const expectedTabs = [mockPageLayoutTab];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabRepository, 'find')
|
||||
.mockResolvedValue(expectedTabs);
|
||||
|
||||
const result = await pageLayoutTabService.findByPageLayoutId(
|
||||
workspaceId,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
expect(pageLayoutTabRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
pageLayoutId,
|
||||
pageLayout: { workspaceId },
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
relations: ['widgets'],
|
||||
withDeleted: false,
|
||||
});
|
||||
expect(result).toEqual(expectedTabs);
|
||||
});
|
||||
|
||||
it('should return empty array when no tabs are found', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutId = 'page-layout-id';
|
||||
|
||||
jest.spyOn(pageLayoutTabRepository, 'find').mockResolvedValue([]);
|
||||
|
||||
const result = await pageLayoutTabService.findByPageLayoutId(
|
||||
workspaceId,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should order tabs by position in ascending order', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutId = 'page-layout-id';
|
||||
const tab1 = { ...mockPageLayoutTab, id: 'tab-1', position: 2 };
|
||||
const tab2 = { ...mockPageLayoutTab, id: 'tab-2', position: 0 };
|
||||
const tab3 = { ...mockPageLayoutTab, id: 'tab-3', position: 1 };
|
||||
const expectedTabs = [tab2, tab3, tab1];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabRepository, 'find')
|
||||
.mockResolvedValue(expectedTabs);
|
||||
|
||||
const result = await pageLayoutTabService.findByPageLayoutId(
|
||||
workspaceId,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
expect(pageLayoutTabRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
pageLayoutId,
|
||||
pageLayout: { workspaceId },
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
relations: ['widgets'],
|
||||
withDeleted: false,
|
||||
});
|
||||
expect(result).toEqual(expectedTabs);
|
||||
});
|
||||
|
||||
it('should include widgets relation', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutId = 'page-layout-id';
|
||||
const widget1 = { ...mockWidget, id: 'widget-1', type: WidgetType.VIEW };
|
||||
const widget2 = {
|
||||
...mockWidget,
|
||||
id: 'widget-2',
|
||||
type: WidgetType.FIELDS,
|
||||
};
|
||||
const tabWithWidgets = {
|
||||
...mockPageLayoutTab,
|
||||
widgets: [widget1, widget2],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabRepository, 'find')
|
||||
.mockResolvedValue([tabWithWidgets]);
|
||||
|
||||
const result = await pageLayoutTabService.findByPageLayoutId(
|
||||
workspaceId,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
expect(result[0].widgets).toHaveLength(2);
|
||||
expect(result[0].widgets[0].id).toEqual('widget-1');
|
||||
expect(result[0].widgets[1].id).toEqual('widget-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByIdOrThrow', () => {
|
||||
it('should return page layout tab when found', async () => {
|
||||
const id = 'page-layout-tab-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabRepository, 'findOne')
|
||||
.mockResolvedValue(mockPageLayoutTab);
|
||||
|
||||
const result = await pageLayoutTabService.findByIdOrThrow(
|
||||
id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
expect(pageLayoutTabRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: expect.anything(),
|
||||
},
|
||||
relations: ['widgets'],
|
||||
});
|
||||
expect(result).toEqual(mockPageLayoutTab);
|
||||
});
|
||||
|
||||
it('should throw exception when page layout tab is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(pageLayoutTabRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
pageLayoutTabService.findByIdOrThrow(id, workspaceId),
|
||||
).rejects.toThrow(PageLayoutTabException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new page layout tab successfully', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutTabData = {
|
||||
id: 'page-layout-tab-id',
|
||||
title: 'New Tab',
|
||||
pageLayoutId: 'page-layout-id',
|
||||
position: 1,
|
||||
};
|
||||
|
||||
const mockPageLayout = {
|
||||
id: 'page-layout-id',
|
||||
applicationId: 'application-id',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockResolvedValue(mockPageLayout as any);
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByIdOrThrow')
|
||||
.mockResolvedValue(mockPageLayoutTab as any);
|
||||
|
||||
jest.spyOn(pageLayoutTabRepository, 'insert').mockResolvedValue({
|
||||
identifiers: [{ id: 'page-layout-tab-id' }],
|
||||
generatedMaps: [],
|
||||
raw: [],
|
||||
});
|
||||
|
||||
const result = await pageLayoutTabService.create(
|
||||
pageLayoutTabData,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
expect(pageLayoutService.findByIdOrThrow).toHaveBeenCalledWith(
|
||||
pageLayoutTabData.pageLayoutId,
|
||||
workspaceId,
|
||||
undefined,
|
||||
);
|
||||
expect(pageLayoutTabRepository.insert).toHaveBeenCalledWith({
|
||||
...pageLayoutTabData,
|
||||
workspaceId,
|
||||
applicationId: 'application-id',
|
||||
universalIdentifier: expect.any(String),
|
||||
});
|
||||
expect(result).toEqual(mockPageLayoutTab);
|
||||
});
|
||||
|
||||
it('should throw an exception when title is not provided', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutTabData = {
|
||||
pageLayoutId: 'page-layout-id',
|
||||
};
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error - we are testing the exception
|
||||
pageLayoutTabService.create(pageLayoutTabData, workspaceId),
|
||||
).rejects.toThrow(PageLayoutTabException);
|
||||
await expect(
|
||||
// @ts-expect-error - we are testing the exception
|
||||
pageLayoutTabService.create(pageLayoutTabData, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception when page layout does not exist', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutTabData = {
|
||||
title: 'New Tab',
|
||||
pageLayoutId: 'non-existent-page-layout-id',
|
||||
};
|
||||
|
||||
jest.spyOn(pageLayoutTabRepository, 'insert').mockResolvedValue({
|
||||
identifiers: [{ id: 'page-layout-tab-id' }],
|
||||
generatedMaps: [],
|
||||
raw: [],
|
||||
});
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockRejectedValue(
|
||||
new PageLayoutException(
|
||||
'Page layout not found',
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
pageLayoutTabService.create(pageLayoutTabData, workspaceId),
|
||||
).rejects.toThrow(PageLayoutTabException);
|
||||
await expect(
|
||||
pageLayoutTabService.create(pageLayoutTabData, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception when page layout is not found', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutTabData = {
|
||||
title: 'New Tab',
|
||||
pageLayoutId: 'non-existent-page-layout-id',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockRejectedValue(new Error('Page layout not found'));
|
||||
|
||||
await expect(
|
||||
pageLayoutTabService.create(pageLayoutTabData, workspaceId),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a page layout tab successfully', async () => {
|
||||
const id = 'page-layout-tab-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const updateData = { title: 'Updated Tab' };
|
||||
const updatedTab = { ...mockPageLayoutTab, title: 'Updated Tab' };
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabRepository, 'findOne')
|
||||
.mockResolvedValue(mockPageLayoutTab);
|
||||
|
||||
jest.spyOn(pageLayoutTabRepository, 'update').mockResolvedValue({
|
||||
affected: 1,
|
||||
generatedMaps: [],
|
||||
raw: {},
|
||||
});
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByIdOrThrow')
|
||||
.mockResolvedValue(updatedTab);
|
||||
|
||||
const result = await pageLayoutTabService.update(
|
||||
id,
|
||||
workspaceId,
|
||||
updateData,
|
||||
);
|
||||
|
||||
expect(pageLayoutTabRepository.update).toHaveBeenCalledWith(
|
||||
{ id },
|
||||
updateData,
|
||||
);
|
||||
expect(result).toEqual(updatedTab);
|
||||
});
|
||||
|
||||
it('should throw an exception when tab to update is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const updateData = { title: 'Updated Tab' };
|
||||
|
||||
jest.spyOn(pageLayoutTabRepository, 'update').mockResolvedValue({
|
||||
affected: 1,
|
||||
generatedMaps: [],
|
||||
raw: {},
|
||||
});
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByIdOrThrow')
|
||||
.mockRejectedValue(
|
||||
new PageLayoutTabException(
|
||||
'Page layout tab not found',
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
pageLayoutTabService.update(id, workspaceId, updateData),
|
||||
).rejects.toThrow(PageLayoutTabException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should soft delete a page layout tab successfully', async () => {
|
||||
const id = 'page-layout-tab-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByIdOrThrow')
|
||||
.mockResolvedValue(mockPageLayoutTab);
|
||||
jest
|
||||
.spyOn(pageLayoutTabRepository, 'softDelete')
|
||||
.mockResolvedValue({ affected: 1, generatedMaps: [], raw: {} });
|
||||
|
||||
const result = await pageLayoutTabService.delete(id, workspaceId);
|
||||
|
||||
expect(pageLayoutTabRepository.softDelete).toHaveBeenCalledWith(id);
|
||||
expect(result).toEqual(mockPageLayoutTab);
|
||||
});
|
||||
|
||||
it('should throw an exception when tab to delete is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByIdOrThrow')
|
||||
.mockRejectedValue(
|
||||
new PageLayoutTabException(
|
||||
'Page layout tab not found',
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
pageLayoutTabService.delete(id, workspaceId),
|
||||
).rejects.toThrow(PageLayoutTabException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
it('should permanently delete a page layout tab successfully', async () => {
|
||||
const id = 'page-layout-tab-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabRepository, 'findOne')
|
||||
.mockResolvedValue(mockPageLayoutTab);
|
||||
jest
|
||||
.spyOn(pageLayoutTabRepository, 'delete')
|
||||
.mockResolvedValue({ affected: 1, generatedMaps: [], raw: {} });
|
||||
|
||||
const result = await pageLayoutTabService.destroy(id, workspaceId);
|
||||
|
||||
expect(pageLayoutTabRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
expect(pageLayoutTabRepository.delete).toHaveBeenCalledWith(id);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw an exception when tab to destroy is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(pageLayoutTabRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
pageLayoutTabService.destroy(id, workspaceId),
|
||||
).rejects.toThrow(PageLayoutTabException);
|
||||
await expect(
|
||||
pageLayoutTabService.destroy(id, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restore', () => {
|
||||
it('should restore a deleted page layout tab successfully', async () => {
|
||||
const id = 'page-layout-tab-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const deletedTab = { ...mockPageLayoutTab, deletedAt: new Date() };
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabRepository, 'findOne')
|
||||
.mockResolvedValue(deletedTab);
|
||||
jest
|
||||
.spyOn(pageLayoutTabRepository, 'restore')
|
||||
.mockResolvedValue({ affected: 1, generatedMaps: [], raw: {} });
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByIdOrThrow')
|
||||
.mockResolvedValue(mockPageLayoutTab);
|
||||
|
||||
const result = await pageLayoutTabService.restore(id, workspaceId);
|
||||
|
||||
expect(pageLayoutTabRepository.findOne).toHaveBeenCalledWith({
|
||||
select: {
|
||||
id: true,
|
||||
deletedAt: true,
|
||||
pageLayoutId: true,
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
expect(pageLayoutTabRepository.restore).toHaveBeenCalledWith(id);
|
||||
expect(result).toEqual(mockPageLayoutTab);
|
||||
});
|
||||
|
||||
it('should throw an exception when tab to restore is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(pageLayoutTabRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
pageLayoutTabService.restore(id, workspaceId),
|
||||
).rejects.toThrow(PageLayoutTabException);
|
||||
await expect(
|
||||
pageLayoutTabService.restore(id, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception when tab is not deleted', async () => {
|
||||
const id = 'page-layout-tab-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const notDeletedTab = { ...mockPageLayoutTab, deletedAt: null };
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabRepository, 'findOne')
|
||||
.mockResolvedValue(notDeletedTab);
|
||||
|
||||
await expect(
|
||||
pageLayoutTabService.restore(id, workspaceId),
|
||||
).rejects.toThrow(PageLayoutTabException);
|
||||
await expect(
|
||||
pageLayoutTabService.restore(id, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception when parent page layout is not accessible', async () => {
|
||||
const id = 'page-layout-tab-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const deletedTab = {
|
||||
...mockPageLayoutTab,
|
||||
deletedAt: new Date(),
|
||||
pageLayoutId: 'deleted-page-layout-id',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabRepository, 'findOne')
|
||||
.mockResolvedValue(deletedTab);
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockRejectedValue(
|
||||
new PageLayoutException(
|
||||
'Page layout not found',
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
pageLayoutTabService.restore(id, workspaceId),
|
||||
).rejects.toThrow(PageLayoutTabException);
|
||||
await expect(
|
||||
pageLayoutTabService.restore(id, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA,
|
||||
);
|
||||
await expect(
|
||||
pageLayoutTabService.restore(id, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'message',
|
||||
generatePageLayoutTabExceptionMessage(
|
||||
PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
|
||||
expect(pageLayoutService.findByIdOrThrow).toHaveBeenCalledWith(
|
||||
'deleted-page-layout-id',
|
||||
workspaceId,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
-764
@@ -1,764 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { DataSource, type EntityManager } from 'typeorm';
|
||||
|
||||
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type UpdatePageLayoutTabWithWidgetsInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-tab-with-widgets.input';
|
||||
import { type UpdatePageLayoutWidgetWithIdInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-widget-with-id.input';
|
||||
import { type PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity';
|
||||
import { type PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-widget.entity';
|
||||
import { type PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout/enums/widget-type.enum';
|
||||
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout/services/page-layout-tab.service';
|
||||
import { PageLayoutUpdateService } from 'src/engine/metadata-modules/page-layout/services/page-layout-update.service';
|
||||
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout/services/page-layout-widget.service';
|
||||
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
|
||||
describe('PageLayoutUpdateService', () => {
|
||||
let pageLayoutUpdateService: PageLayoutUpdateService;
|
||||
let pageLayoutService: PageLayoutService;
|
||||
let pageLayoutTabService: PageLayoutTabService;
|
||||
let pageLayoutWidgetService: PageLayoutWidgetService;
|
||||
let mockTransactionManager: EntityManager;
|
||||
let mockDataSource: DataSource;
|
||||
|
||||
const mockPageLayout = {
|
||||
id: 'page-layout-id',
|
||||
name: 'Test Page Layout',
|
||||
workspaceId: 'workspace-id',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
tabs: [],
|
||||
workspace: {} as WorkspaceEntity,
|
||||
objectMetadata: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
universalIdentifier: 'universal-identifier',
|
||||
applicationId: 'application-id',
|
||||
application: {} as ApplicationEntity,
|
||||
} as PageLayoutEntity;
|
||||
|
||||
const mockTab = {
|
||||
id: 'tab-1',
|
||||
title: 'Test Tab',
|
||||
position: 0,
|
||||
pageLayoutId: 'page-layout-id',
|
||||
workspaceId: 'workspace-id',
|
||||
workspace: {} as WorkspaceEntity,
|
||||
pageLayout: {} as PageLayoutEntity,
|
||||
widgets: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
application: {} as ApplicationEntity,
|
||||
applicationId: 'application-id',
|
||||
universalIdentifier: 'universal-identifier',
|
||||
} as PageLayoutTabEntity;
|
||||
|
||||
const mockWidget = {
|
||||
id: 'widget-1',
|
||||
title: 'Test Widget',
|
||||
type: WidgetType.VIEW,
|
||||
pageLayoutTabId: 'tab-1',
|
||||
workspaceId: 'workspace-id',
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
configuration: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
} as PageLayoutWidgetEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockTransactionManager = {} as EntityManager;
|
||||
mockDataSource = {
|
||||
createQueryRunner: jest.fn().mockReturnValue({
|
||||
connect: jest.fn(),
|
||||
startTransaction: jest.fn(),
|
||||
commitTransaction: jest.fn(),
|
||||
rollbackTransaction: jest.fn(),
|
||||
release: jest.fn(),
|
||||
manager: mockTransactionManager,
|
||||
}),
|
||||
} as unknown as DataSource;
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PageLayoutUpdateService,
|
||||
{
|
||||
provide: PageLayoutService,
|
||||
useValue: {
|
||||
findByIdOrThrow: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PageLayoutTabService,
|
||||
useValue: {
|
||||
findByPageLayoutId: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PageLayoutWidgetService,
|
||||
useValue: {
|
||||
findByPageLayoutTabId: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DataSource,
|
||||
useValue: mockDataSource,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
pageLayoutUpdateService = module.get<PageLayoutUpdateService>(
|
||||
PageLayoutUpdateService,
|
||||
);
|
||||
pageLayoutService = module.get<PageLayoutService>(PageLayoutService);
|
||||
pageLayoutTabService =
|
||||
module.get<PageLayoutTabService>(PageLayoutTabService);
|
||||
pageLayoutWidgetService = module.get<PageLayoutWidgetService>(
|
||||
PageLayoutWidgetService,
|
||||
);
|
||||
});
|
||||
|
||||
describe('updatePageLayoutWithTabs', () => {
|
||||
it('should update page layout and handle tabs', async () => {
|
||||
const id = 'page-layout-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const input = {
|
||||
name: 'Updated Page Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'Updated Tab',
|
||||
position: 0,
|
||||
widgets: [
|
||||
{
|
||||
id: 'widget-1',
|
||||
title: 'Updated Widget',
|
||||
type: WidgetType.VIEW,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
pageLayoutTabId: 'tab-1',
|
||||
objectMetadataId: null,
|
||||
configuration: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const updatedPageLayout = {
|
||||
...mockPageLayout,
|
||||
name: 'Updated Page Layout',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockResolvedValueOnce(mockPageLayout)
|
||||
.mockResolvedValueOnce(updatedPageLayout);
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'update')
|
||||
.mockResolvedValue(updatedPageLayout);
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByPageLayoutId')
|
||||
.mockResolvedValue([mockTab]);
|
||||
jest.spyOn(pageLayoutTabService, 'update').mockResolvedValue({
|
||||
...mockTab,
|
||||
title: 'Updated Tab',
|
||||
});
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetService, 'findByPageLayoutTabId')
|
||||
.mockResolvedValue([mockWidget]);
|
||||
jest.spyOn(pageLayoutWidgetService, 'update').mockResolvedValue({
|
||||
...mockWidget,
|
||||
title: 'Updated Widget',
|
||||
});
|
||||
|
||||
const result = await pageLayoutUpdateService.updatePageLayoutWithTabs({
|
||||
id,
|
||||
workspaceId,
|
||||
input,
|
||||
transactionManager: mockTransactionManager,
|
||||
});
|
||||
|
||||
expect(pageLayoutService.findByIdOrThrow).toHaveBeenCalledWith(
|
||||
id,
|
||||
workspaceId,
|
||||
mockTransactionManager,
|
||||
);
|
||||
expect(pageLayoutService.update).toHaveBeenCalledWith(
|
||||
id,
|
||||
workspaceId,
|
||||
{
|
||||
name: 'Updated Page Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
},
|
||||
mockTransactionManager,
|
||||
);
|
||||
expect(result).toEqual(updatedPageLayout);
|
||||
});
|
||||
|
||||
it('should throw error when page layout is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const input = {
|
||||
name: 'Updated Page Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
tabs: [],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockRejectedValue(new Error('Page layout not found'));
|
||||
|
||||
await expect(
|
||||
pageLayoutUpdateService.updatePageLayoutWithTabs({
|
||||
id,
|
||||
workspaceId,
|
||||
input,
|
||||
transactionManager: mockTransactionManager,
|
||||
}),
|
||||
).rejects.toThrow('Page layout not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePageLayoutTabs', () => {
|
||||
it('should create new tabs', async () => {
|
||||
const pageLayoutId = 'page-layout-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const tabs = [
|
||||
{
|
||||
id: 'new-tab-id',
|
||||
title: 'New Tab',
|
||||
position: 0,
|
||||
widgets: [],
|
||||
},
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByPageLayoutId')
|
||||
.mockResolvedValue([]);
|
||||
jest.spyOn(pageLayoutTabService, 'create').mockResolvedValue({
|
||||
...mockTab,
|
||||
id: 'new-tab-id',
|
||||
title: 'New Tab',
|
||||
position: 0,
|
||||
});
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetService, 'findByPageLayoutTabId')
|
||||
.mockResolvedValue([]);
|
||||
|
||||
await pageLayoutUpdateService['updatePageLayoutTabs']({
|
||||
pageLayoutId,
|
||||
workspaceId,
|
||||
tabs,
|
||||
transactionManager: mockTransactionManager,
|
||||
});
|
||||
|
||||
expect(pageLayoutTabService.create).toHaveBeenCalledWith(
|
||||
{
|
||||
id: 'new-tab-id',
|
||||
title: 'New Tab',
|
||||
position: 0,
|
||||
pageLayoutId,
|
||||
widgets: [],
|
||||
},
|
||||
workspaceId,
|
||||
mockTransactionManager,
|
||||
);
|
||||
});
|
||||
|
||||
it('should update existing tabs', async () => {
|
||||
const pageLayoutId = 'page-layout-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const tabs = [
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'Updated Tab',
|
||||
position: 0,
|
||||
widgets: [],
|
||||
},
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByPageLayoutId')
|
||||
.mockResolvedValue([mockTab]);
|
||||
jest.spyOn(pageLayoutTabService, 'update').mockResolvedValue({
|
||||
...mockTab,
|
||||
title: 'Updated Tab',
|
||||
});
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetService, 'findByPageLayoutTabId')
|
||||
.mockResolvedValue([]);
|
||||
|
||||
await pageLayoutUpdateService['updatePageLayoutTabs']({
|
||||
pageLayoutId,
|
||||
workspaceId,
|
||||
tabs,
|
||||
transactionManager: mockTransactionManager,
|
||||
});
|
||||
|
||||
expect(pageLayoutTabService.update).toHaveBeenCalledWith(
|
||||
'tab-1',
|
||||
workspaceId,
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'Updated Tab',
|
||||
position: 0,
|
||||
},
|
||||
mockTransactionManager,
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete removed tabs', async () => {
|
||||
const pageLayoutId = 'page-layout-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const tabs: UpdatePageLayoutTabWithWidgetsInput[] = [];
|
||||
const existingTabs = [mockTab];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByPageLayoutId')
|
||||
.mockResolvedValue(existingTabs);
|
||||
jest.spyOn(pageLayoutTabService, 'delete').mockResolvedValue(mockTab);
|
||||
|
||||
await pageLayoutUpdateService['updatePageLayoutTabs']({
|
||||
pageLayoutId,
|
||||
workspaceId,
|
||||
tabs,
|
||||
transactionManager: mockTransactionManager,
|
||||
});
|
||||
|
||||
expect(pageLayoutTabService.delete).toHaveBeenCalledWith(
|
||||
'tab-1',
|
||||
workspaceId,
|
||||
mockTransactionManager,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle tabs with mixed operations (create, update, delete)', async () => {
|
||||
const pageLayoutId = 'page-layout-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const tabs = [
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'Updated Tab',
|
||||
position: 0,
|
||||
widgets: [],
|
||||
},
|
||||
{
|
||||
id: 'new-tab-id',
|
||||
title: 'New Tab',
|
||||
position: 1,
|
||||
widgets: [],
|
||||
},
|
||||
];
|
||||
const existingTabs = [
|
||||
mockTab,
|
||||
{
|
||||
...mockTab,
|
||||
id: 'tab-to-delete',
|
||||
title: 'Tab to Delete',
|
||||
},
|
||||
] as PageLayoutTabEntity[];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByPageLayoutId')
|
||||
.mockResolvedValue(existingTabs);
|
||||
jest.spyOn(pageLayoutTabService, 'update').mockResolvedValue({
|
||||
...mockTab,
|
||||
title: 'Updated Tab',
|
||||
});
|
||||
jest.spyOn(pageLayoutTabService, 'create').mockResolvedValue({
|
||||
...mockTab,
|
||||
id: 'new-tab-id',
|
||||
title: 'New Tab',
|
||||
position: 1,
|
||||
});
|
||||
jest.spyOn(pageLayoutTabService, 'delete').mockResolvedValue({
|
||||
...mockTab,
|
||||
id: 'tab-to-delete',
|
||||
title: 'Tab to Delete',
|
||||
});
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetService, 'findByPageLayoutTabId')
|
||||
.mockResolvedValue([]);
|
||||
|
||||
await pageLayoutUpdateService['updatePageLayoutTabs']({
|
||||
pageLayoutId,
|
||||
workspaceId,
|
||||
tabs,
|
||||
transactionManager: mockTransactionManager,
|
||||
});
|
||||
|
||||
expect(pageLayoutTabService.delete).toHaveBeenCalledWith(
|
||||
'tab-to-delete',
|
||||
workspaceId,
|
||||
mockTransactionManager,
|
||||
);
|
||||
expect(pageLayoutTabService.update).toHaveBeenCalledWith(
|
||||
'tab-1',
|
||||
workspaceId,
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'Updated Tab',
|
||||
position: 0,
|
||||
},
|
||||
mockTransactionManager,
|
||||
);
|
||||
expect(pageLayoutTabService.create).toHaveBeenCalledWith(
|
||||
{
|
||||
id: 'new-tab-id',
|
||||
title: 'New Tab',
|
||||
position: 1,
|
||||
pageLayoutId,
|
||||
widgets: [],
|
||||
},
|
||||
workspaceId,
|
||||
mockTransactionManager,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateWidgetsForTab', () => {
|
||||
it('should create new widgets', async () => {
|
||||
const tabId = 'tab-1';
|
||||
const workspaceId = 'workspace-id';
|
||||
const widgets = [
|
||||
{
|
||||
id: 'new-widget-id',
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: null,
|
||||
title: 'New Widget',
|
||||
type: WidgetType.VIEW,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
},
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetService, 'findByPageLayoutTabId')
|
||||
.mockResolvedValue([]);
|
||||
jest.spyOn(pageLayoutWidgetService, 'create').mockResolvedValue({
|
||||
...mockWidget,
|
||||
id: 'new-widget-id',
|
||||
title: 'New Widget',
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: null,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
});
|
||||
|
||||
await pageLayoutUpdateService['updateWidgetsForTab']({
|
||||
tabId,
|
||||
widgets,
|
||||
workspaceId,
|
||||
transactionManager: mockTransactionManager,
|
||||
});
|
||||
|
||||
expect(pageLayoutWidgetService.create).toHaveBeenCalledWith(
|
||||
{
|
||||
id: 'new-widget-id',
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: null,
|
||||
title: 'New Widget',
|
||||
type: WidgetType.VIEW,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
},
|
||||
workspaceId,
|
||||
mockTransactionManager,
|
||||
);
|
||||
});
|
||||
|
||||
it('should update existing widgets', async () => {
|
||||
const tabId = 'tab-1';
|
||||
const workspaceId = 'workspace-id';
|
||||
const widgets = [
|
||||
{
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: null,
|
||||
id: 'widget-1',
|
||||
title: 'Updated Widget',
|
||||
type: WidgetType.VIEW,
|
||||
gridPosition: { row: 1, column: 1, rowSpan: 2, columnSpan: 2 },
|
||||
},
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetService, 'findByPageLayoutTabId')
|
||||
.mockResolvedValue([mockWidget]);
|
||||
jest.spyOn(pageLayoutWidgetService, 'update').mockResolvedValue({
|
||||
...mockWidget,
|
||||
title: 'Updated Widget',
|
||||
gridPosition: { row: 1, column: 1, rowSpan: 2, columnSpan: 2 },
|
||||
});
|
||||
|
||||
await pageLayoutUpdateService['updateWidgetsForTab']({
|
||||
tabId,
|
||||
widgets,
|
||||
workspaceId,
|
||||
transactionManager: mockTransactionManager,
|
||||
});
|
||||
|
||||
expect(pageLayoutWidgetService.update).toHaveBeenCalledWith(
|
||||
'widget-1',
|
||||
workspaceId,
|
||||
{
|
||||
id: 'widget-1',
|
||||
title: 'Updated Widget',
|
||||
type: WidgetType.VIEW,
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: null,
|
||||
gridPosition: { row: 1, column: 1, rowSpan: 2, columnSpan: 2 },
|
||||
},
|
||||
mockTransactionManager,
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete removed widgets', async () => {
|
||||
const tabId = 'tab-1';
|
||||
const workspaceId = 'workspace-id';
|
||||
const widgets: UpdatePageLayoutWidgetWithIdInput[] = [];
|
||||
const existingWidgets = [mockWidget];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetService, 'findByPageLayoutTabId')
|
||||
.mockResolvedValue(existingWidgets);
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetService, 'delete')
|
||||
.mockResolvedValue(mockWidget);
|
||||
|
||||
await pageLayoutUpdateService['updateWidgetsForTab']({
|
||||
tabId,
|
||||
widgets,
|
||||
workspaceId,
|
||||
transactionManager: mockTransactionManager,
|
||||
});
|
||||
|
||||
expect(pageLayoutWidgetService.delete).toHaveBeenCalledWith(
|
||||
'widget-1',
|
||||
workspaceId,
|
||||
mockTransactionManager,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle widgets with mixed operations (create, update, delete)', async () => {
|
||||
const tabId = 'tab-1';
|
||||
const workspaceId = 'workspace-id';
|
||||
const widgets = [
|
||||
{
|
||||
id: 'widget-1',
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: null,
|
||||
title: 'Updated Widget',
|
||||
type: WidgetType.VIEW,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
},
|
||||
{
|
||||
id: 'new-widget-id',
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: null,
|
||||
title: 'New Widget',
|
||||
type: WidgetType.FIELDS,
|
||||
gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 2 },
|
||||
},
|
||||
];
|
||||
const existingWidgets = [
|
||||
mockWidget,
|
||||
{
|
||||
...mockWidget,
|
||||
id: 'widget-to-delete',
|
||||
title: 'Widget to Delete',
|
||||
},
|
||||
] as PageLayoutWidgetEntity[];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetService, 'findByPageLayoutTabId')
|
||||
.mockResolvedValue(existingWidgets);
|
||||
jest.spyOn(pageLayoutWidgetService, 'update').mockResolvedValue({
|
||||
...mockWidget,
|
||||
title: 'Updated Widget',
|
||||
});
|
||||
jest.spyOn(pageLayoutWidgetService, 'create').mockResolvedValue({
|
||||
...mockWidget,
|
||||
id: 'new-widget-id',
|
||||
title: 'New Widget',
|
||||
type: WidgetType.FIELDS,
|
||||
gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 2 },
|
||||
});
|
||||
jest.spyOn(pageLayoutWidgetService, 'delete').mockResolvedValue({
|
||||
...mockWidget,
|
||||
id: 'widget-to-delete',
|
||||
title: 'Widget to Delete',
|
||||
});
|
||||
|
||||
await pageLayoutUpdateService['updateWidgetsForTab']({
|
||||
tabId,
|
||||
widgets,
|
||||
workspaceId,
|
||||
transactionManager: mockTransactionManager,
|
||||
});
|
||||
|
||||
expect(pageLayoutWidgetService.delete).toHaveBeenCalledWith(
|
||||
'widget-to-delete',
|
||||
workspaceId,
|
||||
mockTransactionManager,
|
||||
);
|
||||
expect(pageLayoutWidgetService.update).toHaveBeenCalledWith(
|
||||
'widget-1',
|
||||
workspaceId,
|
||||
{
|
||||
id: 'widget-1',
|
||||
title: 'Updated Widget',
|
||||
type: WidgetType.VIEW,
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: null,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
},
|
||||
mockTransactionManager,
|
||||
);
|
||||
expect(pageLayoutWidgetService.create).toHaveBeenCalledWith(
|
||||
{
|
||||
id: 'new-widget-id',
|
||||
title: 'New Widget',
|
||||
type: WidgetType.FIELDS,
|
||||
pageLayoutTabId: tabId,
|
||||
objectMetadataId: null,
|
||||
configuration: null,
|
||||
gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 2 },
|
||||
},
|
||||
workspaceId,
|
||||
mockTransactionManager,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration scenarios', () => {
|
||||
it('should handle complete page layout update with nested tabs and widgets', async () => {
|
||||
const id = 'page-layout-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const input = {
|
||||
name: 'Complete Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
widgets: [
|
||||
{
|
||||
id: 'widget-1',
|
||||
title: 'Widget 1',
|
||||
type: WidgetType.VIEW,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
pageLayoutTabId: 'tab-1',
|
||||
objectMetadataId: null,
|
||||
configuration: null,
|
||||
},
|
||||
{
|
||||
id: 'widget-2',
|
||||
title: 'Widget 2',
|
||||
type: WidgetType.FIELDS,
|
||||
gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 2 },
|
||||
pageLayoutTabId: 'tab-1',
|
||||
objectMetadataId: null,
|
||||
configuration: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tab-2',
|
||||
title: 'Tab 2',
|
||||
position: 1,
|
||||
widgets: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const existingTabs = [mockTab];
|
||||
const existingWidgets = [mockWidget];
|
||||
const completeLayout = { ...mockPageLayout, name: 'Complete Layout' };
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockResolvedValueOnce(mockPageLayout)
|
||||
.mockResolvedValueOnce(completeLayout);
|
||||
jest.spyOn(pageLayoutService, 'update').mockResolvedValue(completeLayout);
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByPageLayoutId')
|
||||
.mockResolvedValue(existingTabs);
|
||||
jest.spyOn(pageLayoutTabService, 'update').mockResolvedValue({
|
||||
...mockTab,
|
||||
id: 'tab-1',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
});
|
||||
jest.spyOn(pageLayoutTabService, 'create').mockResolvedValue({
|
||||
...mockTab,
|
||||
id: 'tab-2',
|
||||
title: 'Tab 2',
|
||||
position: 1,
|
||||
});
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetService, 'findByPageLayoutTabId')
|
||||
.mockResolvedValue(existingWidgets);
|
||||
jest.spyOn(pageLayoutWidgetService, 'update').mockResolvedValue({
|
||||
...mockWidget,
|
||||
id: 'widget-1',
|
||||
title: 'Widget 1',
|
||||
});
|
||||
jest.spyOn(pageLayoutWidgetService, 'create').mockResolvedValue({
|
||||
...mockWidget,
|
||||
id: 'widget-2',
|
||||
title: 'Widget 2',
|
||||
type: WidgetType.FIELDS,
|
||||
gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 2 },
|
||||
});
|
||||
|
||||
const result = await pageLayoutUpdateService.updatePageLayoutWithTabs({
|
||||
id,
|
||||
workspaceId,
|
||||
input,
|
||||
transactionManager: mockTransactionManager,
|
||||
});
|
||||
|
||||
expect(pageLayoutService.update).toHaveBeenCalledWith(
|
||||
id,
|
||||
workspaceId,
|
||||
{
|
||||
name: 'Complete Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
},
|
||||
mockTransactionManager,
|
||||
);
|
||||
expect(pageLayoutTabService.update).toHaveBeenCalled();
|
||||
expect(pageLayoutTabService.create).toHaveBeenCalled();
|
||||
expect(pageLayoutWidgetService.update).toHaveBeenCalled();
|
||||
expect(pageLayoutWidgetService.create).toHaveBeenCalled();
|
||||
expect(result).toEqual(completeLayout);
|
||||
});
|
||||
});
|
||||
});
|
||||
-615
@@ -1,615 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { IsNull, type Repository } from 'typeorm';
|
||||
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-widget.entity';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout/enums/widget-type.enum';
|
||||
import {
|
||||
PageLayoutTabException,
|
||||
PageLayoutTabExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-tab.exception';
|
||||
import {
|
||||
generatePageLayoutWidgetExceptionMessage,
|
||||
PageLayoutWidgetException,
|
||||
PageLayoutWidgetExceptionCode,
|
||||
PageLayoutWidgetExceptionMessageKey,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-widget.exception';
|
||||
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout/services/page-layout-tab.service';
|
||||
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout/services/page-layout-widget.service';
|
||||
|
||||
describe('PageLayoutWidgetService', () => {
|
||||
let pageLayoutWidgetService: PageLayoutWidgetService;
|
||||
let pageLayoutWidgetRepository: Repository<PageLayoutWidgetEntity>;
|
||||
let pageLayoutTabService: PageLayoutTabService;
|
||||
|
||||
const mockPageLayoutWidget = {
|
||||
id: 'page-layout-widget-id',
|
||||
title: 'Test Widget',
|
||||
type: WidgetType.VIEW,
|
||||
pageLayoutTabId: 'page-layout-tab-id',
|
||||
pageLayoutTab: {} as any,
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
objectMetadata: null,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
configuration: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
} as PageLayoutWidgetEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PageLayoutWidgetService,
|
||||
{
|
||||
provide: getRepositoryToken(PageLayoutWidgetEntity),
|
||||
useValue: {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
insert: jest.fn(),
|
||||
update: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
restore: jest.fn(),
|
||||
manager: {
|
||||
getRepository: jest.fn().mockReturnValue({
|
||||
findOne: jest.fn(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PageLayoutTabService,
|
||||
useValue: {
|
||||
findByIdOrThrow: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: FeatureFlagService,
|
||||
useValue: {
|
||||
isFeatureEnabled: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
pageLayoutWidgetService = module.get<PageLayoutWidgetService>(
|
||||
PageLayoutWidgetService,
|
||||
);
|
||||
pageLayoutWidgetRepository = module.get<Repository<PageLayoutWidgetEntity>>(
|
||||
getRepositoryToken(PageLayoutWidgetEntity),
|
||||
);
|
||||
pageLayoutTabService =
|
||||
module.get<PageLayoutTabService>(PageLayoutTabService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(pageLayoutWidgetService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findByPageLayoutTabId', () => {
|
||||
it('should return page layout widgets for a page layout tab id', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutTabId = 'page-layout-tab-id';
|
||||
const expectedWidgets = [mockPageLayoutWidget];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository, 'find')
|
||||
.mockResolvedValue(expectedWidgets);
|
||||
|
||||
const result = await pageLayoutWidgetService.findByPageLayoutTabId(
|
||||
workspaceId,
|
||||
pageLayoutTabId,
|
||||
);
|
||||
|
||||
expect(pageLayoutWidgetRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
pageLayoutTabId,
|
||||
workspaceId,
|
||||
},
|
||||
order: { createdAt: 'ASC' },
|
||||
withDeleted: false,
|
||||
});
|
||||
expect(result).toEqual(expectedWidgets);
|
||||
});
|
||||
|
||||
it('should return empty array when no widgets are found', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutTabId = 'page-layout-tab-id';
|
||||
|
||||
jest.spyOn(pageLayoutWidgetRepository, 'find').mockResolvedValue([]);
|
||||
|
||||
const result = await pageLayoutWidgetService.findByPageLayoutTabId(
|
||||
workspaceId,
|
||||
pageLayoutTabId,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByIdOrThrow', () => {
|
||||
it('should return page layout widget when found', async () => {
|
||||
const id = 'page-layout-widget-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository, 'findOne')
|
||||
.mockResolvedValue(mockPageLayoutWidget);
|
||||
|
||||
const result = await pageLayoutWidgetService.findByIdOrThrow(
|
||||
id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
expect(pageLayoutWidgetRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
expect(result).toEqual(mockPageLayoutWidget);
|
||||
});
|
||||
|
||||
it('should throw exception when page layout widget is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(pageLayoutWidgetRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
pageLayoutWidgetService.findByIdOrThrow(id, workspaceId),
|
||||
).rejects.toThrow(PageLayoutWidgetException);
|
||||
await expect(
|
||||
pageLayoutWidgetService.findByIdOrThrow(id, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutWidgetExceptionCode.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
const validPageLayoutWidgetData = {
|
||||
id: 'page-layout-widget-id',
|
||||
title: 'New Widget',
|
||||
pageLayoutTabId: 'page-layout-tab-id',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
type: WidgetType.VIEW,
|
||||
};
|
||||
|
||||
it('should create a new page layout widget successfully', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
const mockPageLayoutTab = {
|
||||
id: 'page-layout-tab-id',
|
||||
pageLayout: {
|
||||
id: 'page-layout-id',
|
||||
applicationId: 'application-id',
|
||||
},
|
||||
};
|
||||
|
||||
const tabRepository = {
|
||||
findOne: jest.fn().mockResolvedValue(mockPageLayoutTab),
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository.manager, 'getRepository')
|
||||
.mockReturnValue(tabRepository as any);
|
||||
|
||||
jest.spyOn(pageLayoutWidgetRepository, 'insert').mockResolvedValue({
|
||||
identifiers: [{ id: 'page-layout-widget-id' }],
|
||||
generatedMaps: [],
|
||||
raw: [],
|
||||
});
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetService, 'findByIdOrThrow')
|
||||
.mockResolvedValue(mockPageLayoutWidget);
|
||||
|
||||
const result = await pageLayoutWidgetService.create(
|
||||
validPageLayoutWidgetData,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
expect(tabRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: validPageLayoutWidgetData.pageLayoutTabId,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['pageLayout'],
|
||||
});
|
||||
expect(pageLayoutWidgetRepository.insert).toHaveBeenCalledWith({
|
||||
...validPageLayoutWidgetData,
|
||||
workspaceId,
|
||||
applicationId: 'application-id',
|
||||
universalIdentifier: expect.any(String),
|
||||
});
|
||||
expect(result).toEqual(mockPageLayoutWidget);
|
||||
});
|
||||
|
||||
it('should throw an exception when title is not provided', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutWidgetData = {
|
||||
...validPageLayoutWidgetData,
|
||||
title: undefined,
|
||||
};
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error - we are testing the exception
|
||||
pageLayoutWidgetService.create(pageLayoutWidgetData, workspaceId),
|
||||
).rejects.toThrow(PageLayoutWidgetException);
|
||||
await expect(
|
||||
// @ts-expect-error - we are testing the exception
|
||||
pageLayoutWidgetService.create(pageLayoutWidgetData, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception when pageLayoutTabId is not provided', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutWidgetData = {
|
||||
...validPageLayoutWidgetData,
|
||||
pageLayoutTabId: undefined,
|
||||
};
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error - we are testing the exception
|
||||
pageLayoutWidgetService.create(pageLayoutWidgetData, workspaceId),
|
||||
).rejects.toThrow(PageLayoutWidgetException);
|
||||
await expect(
|
||||
// @ts-expect-error - we are testing the exception
|
||||
pageLayoutWidgetService.create(pageLayoutWidgetData, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception when gridPosition is not provided', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const pageLayoutWidgetData = {
|
||||
...validPageLayoutWidgetData,
|
||||
gridPosition: undefined,
|
||||
};
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error - we are testing the exception
|
||||
pageLayoutWidgetService.create(pageLayoutWidgetData, workspaceId),
|
||||
).rejects.toThrow(PageLayoutWidgetException);
|
||||
await expect(
|
||||
// @ts-expect-error - we are testing the exception
|
||||
pageLayoutWidgetService.create(pageLayoutWidgetData, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception when page layout tab does not exist', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByIdOrThrow')
|
||||
.mockRejectedValue(
|
||||
new PageLayoutTabException(
|
||||
'Page layout tab not found',
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
pageLayoutWidgetService.create(validPageLayoutWidgetData, workspaceId),
|
||||
).rejects.toThrow(PageLayoutWidgetException);
|
||||
await expect(
|
||||
pageLayoutWidgetService.create(validPageLayoutWidgetData, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
});
|
||||
|
||||
it('should rethrow other errors', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const unexpectedError = new Error('Unexpected error');
|
||||
|
||||
const tabRepository = {
|
||||
findOne: jest.fn().mockRejectedValue(unexpectedError),
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository.manager, 'getRepository')
|
||||
.mockReturnValue(tabRepository as any);
|
||||
|
||||
await expect(
|
||||
pageLayoutWidgetService.create(validPageLayoutWidgetData, workspaceId),
|
||||
).rejects.toThrow(unexpectedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a page layout widget successfully', async () => {
|
||||
const id = 'page-layout-widget-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const updateData = { title: 'Updated Widget' };
|
||||
const updatedWidget = {
|
||||
...mockPageLayoutWidget,
|
||||
title: 'Updated Widget',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository, 'findOne')
|
||||
.mockResolvedValueOnce(mockPageLayoutWidget)
|
||||
.mockResolvedValueOnce(updatedWidget);
|
||||
jest.spyOn(pageLayoutWidgetRepository, 'update').mockResolvedValue({
|
||||
affected: 1,
|
||||
generatedMaps: [],
|
||||
raw: {},
|
||||
});
|
||||
|
||||
const result = await pageLayoutWidgetService.update(
|
||||
id,
|
||||
workspaceId,
|
||||
updateData,
|
||||
);
|
||||
|
||||
expect(pageLayoutWidgetRepository.findOne).toHaveBeenCalledTimes(2);
|
||||
expect(pageLayoutWidgetRepository.findOne).toHaveBeenNthCalledWith(1, {
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
expect(pageLayoutWidgetRepository.findOne).toHaveBeenNthCalledWith(2, {
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
expect(pageLayoutWidgetRepository.update).toHaveBeenCalledWith(
|
||||
{ id },
|
||||
updateData,
|
||||
);
|
||||
expect(result).toEqual(updatedWidget);
|
||||
});
|
||||
|
||||
it('should throw an exception when widget to update is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const updateData = { title: 'Updated Widget' };
|
||||
|
||||
jest.spyOn(pageLayoutWidgetRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
pageLayoutWidgetService.update(id, workspaceId, updateData),
|
||||
).rejects.toThrow(PageLayoutWidgetException);
|
||||
await expect(
|
||||
pageLayoutWidgetService.update(id, workspaceId, updateData),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutWidgetExceptionCode.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should soft delete a page layout widget successfully', async () => {
|
||||
const id = 'page-layout-widget-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository, 'findOne')
|
||||
.mockResolvedValue(mockPageLayoutWidget);
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository, 'softDelete')
|
||||
.mockResolvedValue({ affected: 1, generatedMaps: [], raw: {} });
|
||||
|
||||
const result = await pageLayoutWidgetService.delete(id, workspaceId);
|
||||
|
||||
expect(pageLayoutWidgetRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
expect(pageLayoutWidgetRepository.softDelete).toHaveBeenCalledWith(id);
|
||||
expect(result).toEqual(mockPageLayoutWidget);
|
||||
});
|
||||
|
||||
it('should throw an exception when widget to delete is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(pageLayoutWidgetRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
pageLayoutWidgetService.delete(id, workspaceId),
|
||||
).rejects.toThrow(PageLayoutWidgetException);
|
||||
await expect(
|
||||
pageLayoutWidgetService.delete(id, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutWidgetExceptionCode.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
it('should permanently delete a page layout widget successfully', async () => {
|
||||
const id = 'page-layout-widget-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository, 'findOne')
|
||||
.mockResolvedValue(mockPageLayoutWidget);
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository, 'delete')
|
||||
.mockResolvedValue({ affected: 1, generatedMaps: [], raw: {} });
|
||||
|
||||
const result = await pageLayoutWidgetService.destroy(id, workspaceId);
|
||||
|
||||
expect(pageLayoutWidgetRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
expect(pageLayoutWidgetRepository.delete).toHaveBeenCalledWith(id);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw an exception when widget to destroy is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(pageLayoutWidgetRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
pageLayoutWidgetService.destroy(id, workspaceId),
|
||||
).rejects.toThrow(PageLayoutWidgetException);
|
||||
await expect(
|
||||
pageLayoutWidgetService.destroy(id, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutWidgetExceptionCode.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restore', () => {
|
||||
it('should restore a deleted page layout widget successfully', async () => {
|
||||
const id = 'page-layout-widget-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const deletedWidget = { ...mockPageLayoutWidget, deletedAt: new Date() };
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository, 'findOne')
|
||||
.mockResolvedValueOnce(deletedWidget) // First call in restore method to check if deleted
|
||||
.mockResolvedValueOnce(mockPageLayoutWidget); // Second call in findByIdOrThrow
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository, 'restore')
|
||||
.mockResolvedValue({ affected: 1, generatedMaps: [], raw: {} });
|
||||
|
||||
const result = await pageLayoutWidgetService.restore(id, workspaceId);
|
||||
|
||||
expect(pageLayoutWidgetRepository.findOne).toHaveBeenCalledTimes(2);
|
||||
expect(pageLayoutWidgetRepository.findOne).toHaveBeenNthCalledWith(1, {
|
||||
select: {
|
||||
id: true,
|
||||
deletedAt: true,
|
||||
pageLayoutTabId: true,
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
expect(pageLayoutWidgetRepository.findOne).toHaveBeenNthCalledWith(2, {
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
expect(pageLayoutWidgetRepository.restore).toHaveBeenCalledWith(id);
|
||||
expect(result).toEqual(mockPageLayoutWidget);
|
||||
});
|
||||
|
||||
it('should throw an exception when widget to restore is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(pageLayoutWidgetRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
pageLayoutWidgetService.restore(id, workspaceId),
|
||||
).rejects.toThrow(PageLayoutWidgetException);
|
||||
await expect(
|
||||
pageLayoutWidgetService.restore(id, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutWidgetExceptionCode.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception when widget is not deleted', async () => {
|
||||
const id = 'page-layout-widget-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const notDeletedWidget = { ...mockPageLayoutWidget, deletedAt: null };
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository, 'findOne')
|
||||
.mockResolvedValue(notDeletedWidget);
|
||||
|
||||
await expect(
|
||||
pageLayoutWidgetService.restore(id, workspaceId),
|
||||
).rejects.toThrow(PageLayoutWidgetException);
|
||||
await expect(
|
||||
pageLayoutWidgetService.restore(id, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception when parent tab is not accessible', async () => {
|
||||
const id = 'page-layout-widget-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const deletedWidget = {
|
||||
...mockPageLayoutWidget,
|
||||
deletedAt: new Date(),
|
||||
pageLayoutTabId: 'deleted-tab-id',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutWidgetRepository, 'findOne')
|
||||
.mockResolvedValue(deletedWidget);
|
||||
jest
|
||||
.spyOn(pageLayoutTabService, 'findByIdOrThrow')
|
||||
.mockRejectedValue(
|
||||
new PageLayoutTabException(
|
||||
'Page layout tab not found',
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
pageLayoutWidgetService.restore(id, workspaceId),
|
||||
).rejects.toThrow(PageLayoutWidgetException);
|
||||
await expect(
|
||||
pageLayoutWidgetService.restore(id, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'code',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
await expect(
|
||||
pageLayoutWidgetService.restore(id, workspaceId),
|
||||
).rejects.toHaveProperty(
|
||||
'message',
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
|
||||
expect(pageLayoutTabService.findByIdOrThrow).toHaveBeenCalledWith(
|
||||
'deleted-tab-id',
|
||||
workspaceId,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
-495
@@ -1,495 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { IsNull, type Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type CreatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout.input';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import {
|
||||
PageLayoutException,
|
||||
PageLayoutExceptionCode,
|
||||
PageLayoutExceptionMessageKey,
|
||||
generatePageLayoutExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
|
||||
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
describe('PageLayoutService', () => {
|
||||
let pageLayoutService: PageLayoutService;
|
||||
let pageLayoutRepository: Repository<PageLayoutEntity>;
|
||||
let workspaceRepository: Repository<WorkspaceEntity>;
|
||||
let twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||
|
||||
const mockPageLayout = {
|
||||
id: 'page-layout-id',
|
||||
name: 'Test Page Layout',
|
||||
workspaceId: 'workspace-id',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
tabs: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
} as unknown as PageLayoutEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PageLayoutService,
|
||||
{
|
||||
provide: getRepositoryToken(PageLayoutEntity),
|
||||
useValue: {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
update: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
restore: jest.fn(),
|
||||
insert: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(WorkspaceEntity),
|
||||
useValue: {
|
||||
findOneOrFail: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: TwentyORMGlobalManager,
|
||||
useValue: {
|
||||
getRepositoryForWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
pageLayoutService = module.get<PageLayoutService>(PageLayoutService);
|
||||
pageLayoutRepository = module.get<Repository<PageLayoutEntity>>(
|
||||
getRepositoryToken(PageLayoutEntity),
|
||||
);
|
||||
workspaceRepository = module.get<Repository<WorkspaceEntity>>(
|
||||
getRepositoryToken(WorkspaceEntity),
|
||||
);
|
||||
twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
|
||||
TwentyORMGlobalManager,
|
||||
);
|
||||
});
|
||||
|
||||
describe('findByWorkspaceId', () => {
|
||||
it('should return page layouts for a workspace', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const expectedPageLayouts = [mockPageLayout];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutRepository, 'find')
|
||||
.mockResolvedValue(expectedPageLayouts);
|
||||
|
||||
const result = await pageLayoutService.findByWorkspaceId(workspaceId);
|
||||
|
||||
expect(pageLayoutRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['tabs', 'tabs.widgets'],
|
||||
});
|
||||
expect(result).toEqual(expectedPageLayouts);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByObjectMetadataId', () => {
|
||||
it('should return page layouts for an object metadata id', async () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const objectMetadataId = 'object-metadata-id';
|
||||
const expectedPageLayouts = [mockPageLayout];
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutRepository, 'find')
|
||||
.mockResolvedValue(expectedPageLayouts);
|
||||
|
||||
const result = await pageLayoutService.findByObjectMetadataId(
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
);
|
||||
|
||||
expect(pageLayoutRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['tabs', 'tabs.widgets'],
|
||||
});
|
||||
expect(result).toEqual(expectedPageLayouts);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByIdOrThrow', () => {
|
||||
it('should return a page layout by id', async () => {
|
||||
const id = 'page-layout-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutRepository, 'findOne')
|
||||
.mockResolvedValue(mockPageLayout);
|
||||
|
||||
const result = await pageLayoutService.findByIdOrThrow(id, workspaceId);
|
||||
|
||||
expect(pageLayoutRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['tabs', 'tabs.widgets'],
|
||||
});
|
||||
expect(result).toEqual(mockPageLayout);
|
||||
});
|
||||
|
||||
it('should throw exception when page layout is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(pageLayoutRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
pageLayoutService.findByIdOrThrow(id, workspaceId),
|
||||
).rejects.toThrow(
|
||||
new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
const validPageLayoutData = {
|
||||
id: 'page-layout-id',
|
||||
name: 'Test Page Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
};
|
||||
|
||||
it('should create a page layout successfully', async () => {
|
||||
jest.spyOn(workspaceRepository, 'findOneOrFail').mockResolvedValue({
|
||||
id: 'workspace-id',
|
||||
workspaceCustomApplicationId: 'application-id',
|
||||
} as WorkspaceEntity);
|
||||
jest.spyOn(pageLayoutRepository, 'insert').mockResolvedValue({
|
||||
identifiers: [{ id: 'page-layout-id' }],
|
||||
generatedMaps: [],
|
||||
raw: [],
|
||||
});
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockResolvedValue(mockPageLayout);
|
||||
|
||||
const result = await pageLayoutService.create(
|
||||
validPageLayoutData,
|
||||
'workspace-id',
|
||||
);
|
||||
|
||||
expect(workspaceRepository.findOneOrFail).toHaveBeenCalledWith({
|
||||
where: { id: 'workspace-id' },
|
||||
select: ['workspaceCustomApplicationId'],
|
||||
});
|
||||
expect(pageLayoutRepository.insert).toHaveBeenCalledWith({
|
||||
...validPageLayoutData,
|
||||
workspaceId: 'workspace-id',
|
||||
universalIdentifier: expect.any(String),
|
||||
applicationId: 'application-id',
|
||||
});
|
||||
expect(result).toEqual(mockPageLayout);
|
||||
});
|
||||
|
||||
it('should throw exception when name is missing', async () => {
|
||||
const invalidData = { ...validPageLayoutData, name: undefined };
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
await expect(
|
||||
pageLayoutService.create(
|
||||
invalidData as unknown as CreatePageLayoutInput,
|
||||
workspaceId,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.NAME_REQUIRED,
|
||||
),
|
||||
PageLayoutExceptionCode.INVALID_PAGE_LAYOUT_DATA,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a page layout successfully', async () => {
|
||||
const id = 'page-layout-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const updateData = { name: 'Updated Page Layout' };
|
||||
const updatedPageLayout = { ...mockPageLayout, ...updateData };
|
||||
|
||||
jest.spyOn(pageLayoutRepository, 'update').mockResolvedValue({} as any);
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockResolvedValue(updatedPageLayout);
|
||||
|
||||
const result = await pageLayoutService.update(
|
||||
id,
|
||||
workspaceId,
|
||||
updateData,
|
||||
);
|
||||
|
||||
expect(pageLayoutRepository.update).toHaveBeenCalledWith(
|
||||
{ id, workspaceId },
|
||||
updateData,
|
||||
);
|
||||
expect(pageLayoutService.findByIdOrThrow).toHaveBeenCalledWith(
|
||||
id,
|
||||
workspaceId,
|
||||
undefined,
|
||||
);
|
||||
expect(result).toEqual(updatedPageLayout);
|
||||
});
|
||||
|
||||
it('should throw exception when page layout is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const updateData = { name: 'Updated Page Layout' };
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockRejectedValue(
|
||||
new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
pageLayoutService.update(id, workspaceId, updateData),
|
||||
).rejects.toThrow(
|
||||
new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a page layout successfully', async () => {
|
||||
const id = 'page-layout-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockResolvedValue(mockPageLayout);
|
||||
jest
|
||||
.spyOn(pageLayoutRepository, 'softDelete')
|
||||
.mockResolvedValue({} as any);
|
||||
|
||||
const result = await pageLayoutService.delete(id, workspaceId);
|
||||
|
||||
expect(pageLayoutService.findByIdOrThrow).toHaveBeenCalledWith(
|
||||
id,
|
||||
workspaceId,
|
||||
undefined,
|
||||
);
|
||||
expect(pageLayoutRepository.softDelete).toHaveBeenCalledWith(id);
|
||||
expect(result).toEqual(mockPageLayout);
|
||||
});
|
||||
|
||||
it('should throw exception when page layout is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockRejectedValue(
|
||||
new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
|
||||
await expect(pageLayoutService.delete(id, workspaceId)).rejects.toThrow(
|
||||
new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
it('should destroy a page layout successfully', async () => {
|
||||
const id = 'page-layout-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutRepository, 'findOne')
|
||||
.mockResolvedValue(mockPageLayout);
|
||||
jest.spyOn(pageLayoutRepository, 'delete').mockResolvedValue({} as any);
|
||||
|
||||
const result = await pageLayoutService.destroy(id, workspaceId);
|
||||
|
||||
expect(pageLayoutRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
expect(pageLayoutRepository.delete).toHaveBeenCalledWith(id);
|
||||
expect(result).toEqual(mockPageLayout);
|
||||
});
|
||||
|
||||
it('should throw exception when page layout is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutRepository, 'findOne')
|
||||
.mockRejectedValue(
|
||||
new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
|
||||
await expect(pageLayoutService.destroy(id, workspaceId)).rejects.toThrow(
|
||||
new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should destroy associated dashboards when page layout is a dashboard', async () => {
|
||||
const id = 'page-layout-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const mockDashboardRepository = {
|
||||
find: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
const mockDashboards = [{ id: 'dashboard', pageLayoutId: id }];
|
||||
|
||||
jest.spyOn(pageLayoutRepository, 'findOne').mockResolvedValue({
|
||||
...mockPageLayout,
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
});
|
||||
jest
|
||||
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
|
||||
.mockResolvedValue(mockDashboardRepository as any);
|
||||
jest
|
||||
.spyOn(mockDashboardRepository, 'find')
|
||||
.mockResolvedValue(mockDashboards);
|
||||
jest
|
||||
.spyOn(mockDashboardRepository, 'delete')
|
||||
.mockResolvedValue({} as any);
|
||||
jest.spyOn(pageLayoutRepository, 'delete').mockResolvedValue({} as any);
|
||||
|
||||
const result = await pageLayoutService.destroy(id, workspaceId);
|
||||
|
||||
expect(
|
||||
twentyORMGlobalManager.getRepositoryForWorkspace,
|
||||
).toHaveBeenCalledWith(workspaceId, 'dashboard', {
|
||||
shouldBypassPermissionChecks: true,
|
||||
});
|
||||
expect(mockDashboardRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
pageLayoutId: id,
|
||||
},
|
||||
});
|
||||
expect(mockDashboardRepository.delete).toHaveBeenCalledWith('dashboard');
|
||||
|
||||
expect(pageLayoutRepository.delete).toHaveBeenCalledWith(id);
|
||||
expect(result).toEqual({
|
||||
...mockPageLayout,
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('restore', () => {
|
||||
it('should restore a page layout successfully', async () => {
|
||||
const id = 'page-layout-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
const deletedPageLayout = { ...mockPageLayout, deletedAt: new Date() };
|
||||
|
||||
jest
|
||||
.spyOn(pageLayoutRepository, 'findOne')
|
||||
.mockResolvedValue(deletedPageLayout);
|
||||
jest.spyOn(pageLayoutRepository, 'restore').mockResolvedValue({} as any);
|
||||
jest
|
||||
.spyOn(pageLayoutService, 'findByIdOrThrow')
|
||||
.mockResolvedValue(mockPageLayout);
|
||||
|
||||
const result = await pageLayoutService.restore(id, workspaceId);
|
||||
|
||||
expect(pageLayoutRepository.findOne).toHaveBeenCalledWith({
|
||||
select: {
|
||||
id: true,
|
||||
deletedAt: true,
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
expect(pageLayoutRepository.restore).toHaveBeenCalledWith(id);
|
||||
expect(pageLayoutService.findByIdOrThrow).toHaveBeenCalledWith(
|
||||
id,
|
||||
workspaceId,
|
||||
);
|
||||
expect(result).toEqual(mockPageLayout);
|
||||
});
|
||||
|
||||
it('should throw exception when page layout is not found', async () => {
|
||||
const id = 'non-existent-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(pageLayoutRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(pageLayoutService.restore(id, workspaceId)).rejects.toThrow(
|
||||
new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+285
-193
@@ -1,77 +1,76 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { EntityManager, IsNull, Repository } from 'typeorm';
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
|
||||
import { fromCreatePageLayoutTabInputToFlatPageLayoutTabToCreate } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util';
|
||||
import { fromDeletePageLayoutTabInputToFlatPageLayoutTabOrThrow } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/from-delete-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util';
|
||||
import { fromDestroyPageLayoutTabInputToFlatPageLayoutTabOrThrow } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/from-destroy-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util';
|
||||
import { fromRestorePageLayoutTabInputToFlatPageLayoutTabOrThrow } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/from-restore-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util';
|
||||
import {
|
||||
fromUpdatePageLayoutTabInputToFlatPageLayoutTabToUpdateOrThrow,
|
||||
type UpdatePageLayoutTabInputWithId,
|
||||
} from 'src/engine/metadata-modules/flat-page-layout-tab/utils/from-update-page-layout-tab-input-to-flat-page-layout-tab-to-update-or-throw.util';
|
||||
import { reconstructFlatPageLayoutTabWithWidgets } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/reconstruct-flat-page-layout-tab-with-widgets.util';
|
||||
import { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
import { CreatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout-tab.input';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity';
|
||||
import { UpdatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-tab.input';
|
||||
import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-tab.dto';
|
||||
import {
|
||||
PageLayoutTabException,
|
||||
PageLayoutTabExceptionCode,
|
||||
PageLayoutTabExceptionMessageKey,
|
||||
generatePageLayoutTabExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-tab.exception';
|
||||
import {
|
||||
PageLayoutException,
|
||||
PageLayoutExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
|
||||
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
import { fromFlatPageLayoutTabToPageLayoutTabDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-tab-to-page-layout-tab-dto.util';
|
||||
import { fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-tab-with-widgets-to-page-layout-tab-dto.util';
|
||||
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@Injectable()
|
||||
export class PageLayoutTabService {
|
||||
constructor(
|
||||
@InjectRepository(PageLayoutTabEntity)
|
||||
private readonly pageLayoutTabRepository: Repository<PageLayoutTabEntity>,
|
||||
private readonly pageLayoutService: PageLayoutService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
private getPageLayoutTabRepository(
|
||||
transactionManager?: EntityManager,
|
||||
): Repository<PageLayoutTabEntity> {
|
||||
return transactionManager
|
||||
? transactionManager.getRepository(PageLayoutTabEntity)
|
||||
: this.pageLayoutTabRepository;
|
||||
}
|
||||
|
||||
async findByPageLayoutId(
|
||||
workspaceId: string,
|
||||
pageLayoutId: string,
|
||||
transactionManager?: EntityManager,
|
||||
withDeleted = false,
|
||||
): Promise<PageLayoutTabEntity[]> {
|
||||
const repository = this.getPageLayoutTabRepository(transactionManager);
|
||||
): Promise<PageLayoutTabDTO[]> {
|
||||
const { flatPageLayoutTabMaps, flatPageLayoutWidgetMaps } =
|
||||
await this.getPageLayoutTabFlatEntityMaps(workspaceId);
|
||||
|
||||
return repository.find({
|
||||
where: {
|
||||
pageLayoutId,
|
||||
pageLayout: { workspaceId },
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
relations: ['widgets'],
|
||||
withDeleted,
|
||||
});
|
||||
return Object.values(flatPageLayoutTabMaps.byId)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(tab) => tab.pageLayoutId === pageLayoutId && !isDefined(tab.deletedAt),
|
||||
)
|
||||
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
||||
.map((tab) =>
|
||||
fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto(
|
||||
reconstructFlatPageLayoutTabWithWidgets({
|
||||
tab,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async findByIdOrThrow(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutTabEntity> {
|
||||
const repository = this.getPageLayoutTabRepository(transactionManager);
|
||||
): Promise<PageLayoutTabDTO> {
|
||||
const { flatPageLayoutTabMaps, flatPageLayoutWidgetMaps } =
|
||||
await this.getPageLayoutTabFlatEntityMaps(workspaceId);
|
||||
|
||||
const pageLayoutTab = await repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['widgets'],
|
||||
});
|
||||
const flatTab = flatPageLayoutTabMaps.byId[id];
|
||||
|
||||
if (!isDefined(pageLayoutTab)) {
|
||||
if (!isDefined(flatTab) || isDefined(flatTab.deletedAt)) {
|
||||
throw new PageLayoutTabException(
|
||||
generatePageLayoutTabExceptionMessage(
|
||||
PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
@@ -81,15 +80,31 @@ export class PageLayoutTabService {
|
||||
);
|
||||
}
|
||||
|
||||
return pageLayoutTab;
|
||||
return fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto(
|
||||
reconstructFlatPageLayoutTabWithWidgets({
|
||||
tab: flatTab,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async getPageLayoutTabFlatEntityMaps(workspaceId: string): Promise<{
|
||||
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
}> {
|
||||
return this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutTabMaps', 'flatPageLayoutWidgetMaps'],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async create(
|
||||
pageLayoutTabData: CreatePageLayoutTabInput,
|
||||
createPageLayoutTabInput: CreatePageLayoutTabInput,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutTabEntity> {
|
||||
if (!isDefined(pageLayoutTabData.title)) {
|
||||
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
|
||||
if (!isDefined(createPageLayoutTabInput.title)) {
|
||||
throw new PageLayoutTabException(
|
||||
generatePageLayoutTabExceptionMessage(
|
||||
PageLayoutTabExceptionMessageKey.TITLE_REQUIRED,
|
||||
@@ -98,7 +113,7 @@ export class PageLayoutTabService {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(pageLayoutTabData.pageLayoutId)) {
|
||||
if (!isDefined(createPageLayoutTabInput.pageLayoutId)) {
|
||||
throw new PageLayoutTabException(
|
||||
generatePageLayoutTabExceptionMessage(
|
||||
PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_ID_REQUIRED,
|
||||
@@ -107,118 +122,210 @@ export class PageLayoutTabService {
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const pageLayout = await this.pageLayoutService.findByIdOrThrow(
|
||||
pageLayoutTabData.pageLayoutId,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const repository = this.getPageLayoutTabRepository(transactionManager);
|
||||
|
||||
const insertResult = await repository.insert({
|
||||
...pageLayoutTabData,
|
||||
const flatPageLayoutTabToCreate =
|
||||
fromCreatePageLayoutTabInputToFlatPageLayoutTabToCreate({
|
||||
createPageLayoutTabInput,
|
||||
workspaceId,
|
||||
universalIdentifier: v4(),
|
||||
applicationId: pageLayout.applicationId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
return this.findByIdOrThrow(
|
||||
insertResult.identifiers[0].id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: [flatPageLayoutTabToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while creating page layout tab',
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof PageLayoutException &&
|
||||
error.code === PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND
|
||||
) {
|
||||
throw new PageLayoutTabException(
|
||||
generatePageLayoutTabExceptionMessage(
|
||||
PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { flatPageLayoutTabMaps: recomputedFlatPageLayoutTabMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutTabMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatPageLayoutTabToPageLayoutTabDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: flatPageLayoutTabToCreate.id,
|
||||
flatEntityMaps: recomputedFlatPageLayoutTabMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
updateData: QueryDeepPartialEntity<PageLayoutTabEntity>,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutTabEntity> {
|
||||
const repository = this.getPageLayoutTabRepository(transactionManager);
|
||||
updateData: UpdatePageLayoutTabInput,
|
||||
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
|
||||
const { flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutTabMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const existingTab = await repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
const updatePageLayoutTabInput: UpdatePageLayoutTabInputWithId = {
|
||||
id,
|
||||
update: updateData,
|
||||
};
|
||||
|
||||
if (!isDefined(existingTab)) {
|
||||
throw new PageLayoutTabException(
|
||||
generatePageLayoutTabExceptionMessage(
|
||||
PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
const flatPageLayoutTabToUpdate =
|
||||
fromUpdatePageLayoutTabInputToFlatPageLayoutTabToUpdateOrThrow({
|
||||
updatePageLayoutTabInput,
|
||||
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatPageLayoutTabToUpdate],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while updating page layout tab',
|
||||
);
|
||||
}
|
||||
|
||||
await repository.update({ id }, updateData);
|
||||
const { flatPageLayoutTabMaps: recomputedFlatPageLayoutTabMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutTabMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return this.findByIdOrThrow(id, workspaceId, transactionManager);
|
||||
return fromFlatPageLayoutTabToPageLayoutTabDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedFlatPageLayoutTabMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async delete(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutTabEntity> {
|
||||
const pageLayoutTab = await this.findByIdOrThrow(
|
||||
id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
|
||||
const { flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutTabMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const repository = this.getPageLayoutTabRepository(transactionManager);
|
||||
const flatPageLayoutTabToDelete =
|
||||
fromDeletePageLayoutTabInputToFlatPageLayoutTabOrThrow({
|
||||
deletePageLayoutTabInput: { id },
|
||||
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
|
||||
});
|
||||
|
||||
await repository.softDelete(id);
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatPageLayoutTabToDelete],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
return pageLayoutTab;
|
||||
}
|
||||
|
||||
async destroy(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<boolean> {
|
||||
const repository = this.getPageLayoutTabRepository(transactionManager);
|
||||
|
||||
const pageLayoutTab = await repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (!isDefined(pageLayoutTab)) {
|
||||
throw new PageLayoutTabException(
|
||||
generatePageLayoutTabExceptionMessage(
|
||||
PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while deleting page layout tab',
|
||||
);
|
||||
}
|
||||
|
||||
await repository.delete(id);
|
||||
const { flatPageLayoutTabMaps: recomputedFlatPageLayoutTabMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutTabMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatPageLayoutTabToPageLayoutTabDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedFlatPageLayoutTabMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async destroy(id: string, workspaceId: string): Promise<boolean> {
|
||||
const { flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutTabMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatPageLayoutTabToDestroy =
|
||||
fromDestroyPageLayoutTabInputToFlatPageLayoutTabOrThrow({
|
||||
destroyPageLayoutTabInput: { id },
|
||||
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [flatPageLayoutTabToDestroy],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while destroying page layout tab',
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -226,71 +333,56 @@ export class PageLayoutTabService {
|
||||
async restore(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutTabEntity> {
|
||||
const repository = this.getPageLayoutTabRepository(transactionManager);
|
||||
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
|
||||
const { flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutTabMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const pageLayoutTab = await repository.findOne({
|
||||
select: {
|
||||
id: true,
|
||||
deletedAt: true,
|
||||
pageLayoutId: true,
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
const flatPageLayoutTabToRestore =
|
||||
fromRestorePageLayoutTabInputToFlatPageLayoutTabOrThrow({
|
||||
restorePageLayoutTabInput: { id },
|
||||
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(pageLayoutTab)) {
|
||||
throw new PageLayoutTabException(
|
||||
generatePageLayoutTabExceptionMessage(
|
||||
PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatPageLayoutTabToRestore],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while restoring page layout tab',
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(pageLayoutTab.deletedAt)) {
|
||||
throw new PageLayoutTabException(
|
||||
generatePageLayoutTabExceptionMessage(
|
||||
PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_DELETED,
|
||||
),
|
||||
PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA,
|
||||
const { flatPageLayoutTabMaps: recomputedFlatPageLayoutTabMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutTabMaps'],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.pageLayoutService.findByIdOrThrow(
|
||||
pageLayoutTab.pageLayoutId,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof PageLayoutException &&
|
||||
error.code === PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND
|
||||
) {
|
||||
throw new PageLayoutTabException(
|
||||
generatePageLayoutTabExceptionMessage(
|
||||
PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
),
|
||||
PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
await repository.restore(id);
|
||||
|
||||
const restoredPageLayoutTab = await this.findByIdOrThrow(
|
||||
id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
return fromFlatPageLayoutTabToPageLayoutTabDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedFlatPageLayoutTabMaps,
|
||||
}),
|
||||
);
|
||||
|
||||
return restoredPageLayoutTab;
|
||||
}
|
||||
}
|
||||
|
||||
+353
-190
@@ -1,137 +1,194 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { computeDiffBetweenObjects, isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource, EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout-widget.input';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-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 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 { 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/dtos/inputs/update-page-layout-tab-with-widgets.input';
|
||||
import { UpdatePageLayoutWidgetWithIdInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-widget-with-id.input';
|
||||
import { UpdatePageLayoutWithTabsInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-widget.entity';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout/services/page-layout-tab.service';
|
||||
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout/services/page-layout-widget.service';
|
||||
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
import { PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto';
|
||||
import {
|
||||
PageLayoutException,
|
||||
PageLayoutExceptionCode,
|
||||
PageLayoutExceptionMessageKey,
|
||||
generatePageLayoutExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
|
||||
import { fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.util';
|
||||
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
type UpdatePageLayoutWithTabsParams = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
input: UpdatePageLayoutWithTabsInput;
|
||||
transactionManager?: EntityManager;
|
||||
};
|
||||
|
||||
type UpdatePageLayoutTabsParams = {
|
||||
pageLayoutId: string;
|
||||
workspaceId: string;
|
||||
tabs: UpdatePageLayoutTabWithWidgetsInput[];
|
||||
transactionManager: EntityManager;
|
||||
};
|
||||
|
||||
type UpdateWidgetsForTabParams = {
|
||||
tabId: string;
|
||||
widgets: UpdatePageLayoutWidgetWithIdInput[];
|
||||
workspaceId: string;
|
||||
transactionManager: EntityManager;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PageLayoutUpdateService {
|
||||
constructor(
|
||||
private readonly pageLayoutService: PageLayoutService,
|
||||
private readonly pageLayoutTabService: PageLayoutTabService,
|
||||
private readonly pageLayoutWidgetService: PageLayoutWidgetService,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
async updatePageLayoutWithTabs({
|
||||
id,
|
||||
workspaceId,
|
||||
input,
|
||||
transactionManager,
|
||||
}: UpdatePageLayoutWithTabsParams): Promise<PageLayoutEntity> {
|
||||
if (!isDefined(transactionManager)) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const result = await this.updatePageLayoutWithTabsWithinTransaction({
|
||||
id,
|
||||
}: UpdatePageLayoutWithTabsParams): Promise<PageLayoutDTO> {
|
||||
const {
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
} =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
input,
|
||||
transactionManager: queryRunner.manager,
|
||||
});
|
||||
flatMapsKeys: [
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
const existingPageLayout = flatPageLayoutMaps.byId[id];
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
if (
|
||||
!isDefined(existingPageLayout) ||
|
||||
isDefined(existingPageLayout.deletedAt)
|
||||
) {
|
||||
throw new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return this.updatePageLayoutWithTabsWithinTransaction({
|
||||
id,
|
||||
workspaceId,
|
||||
input,
|
||||
transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
private async updatePageLayoutWithTabsWithinTransaction({
|
||||
id,
|
||||
workspaceId,
|
||||
input,
|
||||
transactionManager,
|
||||
}: UpdatePageLayoutWithTabsParams & {
|
||||
transactionManager: EntityManager;
|
||||
}): Promise<PageLayoutEntity> {
|
||||
await this.pageLayoutService.findByIdOrThrow(
|
||||
id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { tabs, ...updateData } = input;
|
||||
|
||||
await this.pageLayoutService.update(
|
||||
id,
|
||||
workspaceId,
|
||||
updateData,
|
||||
transactionManager,
|
||||
const flatPageLayoutToUpdate: FlatPageLayout = {
|
||||
...existingPageLayout,
|
||||
name: updateData.name,
|
||||
type: updateData.type,
|
||||
objectMetadataId: updateData.objectMetadataId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const { tabsToCreate, tabsToUpdate, tabsToDelete } =
|
||||
this.computeTabOperations({
|
||||
existingPageLayout,
|
||||
tabs,
|
||||
flatPageLayoutTabMaps,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
const { widgetsToCreate, widgetsToUpdate, widgetsToDelete } =
|
||||
this.computeWidgetOperationsForAllTabs({
|
||||
tabs,
|
||||
flatPageLayoutWidgetMaps,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatPageLayoutToUpdate],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: tabsToCreate,
|
||||
flatEntityToDelete: tabsToDelete,
|
||||
flatEntityToUpdate: tabsToUpdate,
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: widgetsToCreate,
|
||||
flatEntityToDelete: widgetsToDelete,
|
||||
flatEntityToUpdate: widgetsToUpdate,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while updating page layout with tabs',
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
flatPageLayoutMaps: recomputedFlatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps: recomputedFlatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps: recomputedFlatPageLayoutWidgetMaps,
|
||||
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
await this.updatePageLayoutTabs({
|
||||
pageLayoutId: id,
|
||||
workspaceId,
|
||||
tabs,
|
||||
transactionManager,
|
||||
const flatLayout = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedFlatPageLayoutMaps,
|
||||
});
|
||||
|
||||
return this.pageLayoutService.findByIdOrThrow(
|
||||
id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
return fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto(
|
||||
reconstructFlatPageLayoutWithTabsAndWidgets({
|
||||
layout: flatLayout,
|
||||
flatPageLayoutTabMaps: recomputedFlatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps: recomputedFlatPageLayoutWidgetMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async updatePageLayoutTabs({
|
||||
pageLayoutId,
|
||||
workspaceId,
|
||||
private computeTabOperations({
|
||||
existingPageLayout,
|
||||
tabs,
|
||||
transactionManager,
|
||||
}: UpdatePageLayoutTabsParams): Promise<void> {
|
||||
const existingTabs = await this.pageLayoutTabService.findByPageLayoutId(
|
||||
workspaceId,
|
||||
pageLayoutId,
|
||||
transactionManager,
|
||||
true,
|
||||
);
|
||||
flatPageLayoutTabMaps,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
}: {
|
||||
existingPageLayout: FlatPageLayout;
|
||||
tabs: UpdatePageLayoutTabWithWidgetsInput[];
|
||||
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
}): {
|
||||
tabsToCreate: FlatPageLayoutTab[];
|
||||
tabsToUpdate: FlatPageLayoutTab[];
|
||||
tabsToDelete: FlatPageLayoutTab[];
|
||||
} {
|
||||
const existingTabs = Object.values(flatPageLayoutTabMaps.byId)
|
||||
.filter(isDefined)
|
||||
.filter((tab) => tab.pageLayoutId === existingPageLayout.id);
|
||||
|
||||
const {
|
||||
toCreate: entitiesToCreate,
|
||||
@@ -139,7 +196,7 @@ export class PageLayoutUpdateService {
|
||||
toRestoreAndUpdate: entitiesToRestoreAndUpdate,
|
||||
idsToDelete,
|
||||
} = computeDiffBetweenObjects<
|
||||
PageLayoutTabEntity,
|
||||
FlatPageLayoutTab,
|
||||
UpdatePageLayoutTabWithWidgetsInput
|
||||
>({
|
||||
existingObjects: existingTabs,
|
||||
@@ -147,76 +204,139 @@ export class PageLayoutUpdateService {
|
||||
propertiesToCompare: ['title', 'position'],
|
||||
});
|
||||
|
||||
for (const tabId of idsToDelete) {
|
||||
await this.pageLayoutTabService.delete(
|
||||
tabId,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
const now = new Date();
|
||||
|
||||
for (const tabToUpdate of entitiesToUpdate) {
|
||||
const { widgets: _widgets, ...updateData } = tabToUpdate;
|
||||
const tabsToCreate: FlatPageLayoutTab[] = entitiesToCreate.map(
|
||||
(tabInput) => {
|
||||
const tabId = tabInput.id ?? v4();
|
||||
|
||||
await this.pageLayoutTabService.update(
|
||||
tabToUpdate.id,
|
||||
workspaceId,
|
||||
updateData,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
return {
|
||||
id: tabId,
|
||||
title: tabInput.title,
|
||||
position: tabInput.position,
|
||||
pageLayoutId: existingPageLayout.id,
|
||||
workspaceId,
|
||||
createdAt: now.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
deletedAt: null,
|
||||
universalIdentifier: tabId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
widgetIds: [],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
for (const tabToRestoreAndUpdate of entitiesToRestoreAndUpdate) {
|
||||
await this.pageLayoutTabService.restore(
|
||||
tabToRestoreAndUpdate.id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
const tabsToUpdate: FlatPageLayoutTab[] = entitiesToUpdate.map(
|
||||
(tabInput) => {
|
||||
const existingTab = flatPageLayoutTabMaps.byId[tabInput.id];
|
||||
|
||||
const { widgets: _widgets, ...updateData } = tabToRestoreAndUpdate;
|
||||
return {
|
||||
...existingTab!,
|
||||
title: tabInput.title,
|
||||
position: tabInput.position,
|
||||
updatedAt: now.toISOString(),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await this.pageLayoutTabService.update(
|
||||
tabToRestoreAndUpdate.id,
|
||||
workspaceId,
|
||||
updateData,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
const tabsToRestoreAndUpdate: FlatPageLayoutTab[] =
|
||||
entitiesToRestoreAndUpdate.map((tabInput) => {
|
||||
const existingTab = flatPageLayoutTabMaps.byId[tabInput.id];
|
||||
|
||||
for (const tabToCreate of entitiesToCreate) {
|
||||
await this.pageLayoutTabService.create(
|
||||
{
|
||||
...tabToCreate,
|
||||
pageLayoutId,
|
||||
},
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
for (const tabInput of tabs) {
|
||||
await this.updateWidgetsForTab({
|
||||
tabId: tabInput.id,
|
||||
widgets: tabInput.widgets,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
return {
|
||||
...existingTab!,
|
||||
title: tabInput.title,
|
||||
position: tabInput.position,
|
||||
deletedAt: null,
|
||||
updatedAt: now.toISOString(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const tabsToDelete: FlatPageLayoutTab[] = idsToDelete
|
||||
.map((tabId) => {
|
||||
const existingTab = flatPageLayoutTabMaps.byId[tabId];
|
||||
|
||||
if (!isDefined(existingTab)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...existingTab,
|
||||
deletedAt: now.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
return {
|
||||
tabsToCreate,
|
||||
tabsToUpdate: [
|
||||
...tabsToUpdate,
|
||||
...tabsToRestoreAndUpdate,
|
||||
...tabsToDelete,
|
||||
],
|
||||
tabsToDelete: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async updateWidgetsForTab({
|
||||
private computeWidgetOperationsForAllTabs({
|
||||
tabs,
|
||||
flatPageLayoutWidgetMaps,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
}: {
|
||||
tabs: UpdatePageLayoutTabWithWidgetsInput[];
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
}): {
|
||||
widgetsToCreate: FlatPageLayoutWidget[];
|
||||
widgetsToUpdate: FlatPageLayoutWidget[];
|
||||
widgetsToDelete: FlatPageLayoutWidget[];
|
||||
} {
|
||||
const allWidgetsToCreate: FlatPageLayoutWidget[] = [];
|
||||
const allWidgetsToUpdate: FlatPageLayoutWidget[] = [];
|
||||
|
||||
for (const tabInput of tabs) {
|
||||
const { widgetsToCreate, widgetsToUpdate } =
|
||||
this.computeWidgetOperationsForTab({
|
||||
tabId: tabInput.id,
|
||||
widgets: tabInput.widgets,
|
||||
flatPageLayoutWidgetMaps,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
});
|
||||
|
||||
allWidgetsToCreate.push(...widgetsToCreate);
|
||||
allWidgetsToUpdate.push(...widgetsToUpdate);
|
||||
}
|
||||
|
||||
return {
|
||||
widgetsToCreate: allWidgetsToCreate,
|
||||
widgetsToUpdate: allWidgetsToUpdate,
|
||||
widgetsToDelete: [],
|
||||
};
|
||||
}
|
||||
|
||||
private computeWidgetOperationsForTab({
|
||||
tabId,
|
||||
widgets,
|
||||
flatPageLayoutWidgetMaps,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
}: UpdateWidgetsForTabParams): Promise<void> {
|
||||
const existingWidgets =
|
||||
await this.pageLayoutWidgetService.findByPageLayoutTabId(
|
||||
workspaceId,
|
||||
tabId,
|
||||
transactionManager,
|
||||
true,
|
||||
);
|
||||
workspaceCustomApplicationId,
|
||||
}: {
|
||||
tabId: string;
|
||||
widgets: UpdatePageLayoutWidgetWithIdInput[];
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
}): {
|
||||
widgetsToCreate: FlatPageLayoutWidget[];
|
||||
widgetsToUpdate: FlatPageLayoutWidget[];
|
||||
} {
|
||||
const existingWidgets = Object.values(flatPageLayoutWidgetMaps.byId)
|
||||
.filter(isDefined)
|
||||
.filter((widget) => widget.pageLayoutTabId === tabId);
|
||||
|
||||
const {
|
||||
toCreate: entitiesToCreate,
|
||||
@@ -224,7 +344,7 @@ export class PageLayoutUpdateService {
|
||||
toRestoreAndUpdate: entitiesToRestoreAndUpdate,
|
||||
idsToDelete,
|
||||
} = computeDiffBetweenObjects<
|
||||
PageLayoutWidgetEntity,
|
||||
FlatPageLayoutWidget,
|
||||
UpdatePageLayoutWidgetWithIdInput
|
||||
>({
|
||||
existingObjects: existingWidgets,
|
||||
@@ -239,44 +359,87 @@ export class PageLayoutUpdateService {
|
||||
],
|
||||
});
|
||||
|
||||
for (const widgetId of idsToDelete) {
|
||||
await this.pageLayoutWidgetService.delete(
|
||||
widgetId,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
const now = new Date();
|
||||
|
||||
for (const widgetUpdate of entitiesToUpdate) {
|
||||
await this.pageLayoutWidgetService.update(
|
||||
widgetUpdate.id,
|
||||
workspaceId,
|
||||
widgetUpdate,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
const widgetsToCreate: FlatPageLayoutWidget[] = entitiesToCreate.map(
|
||||
(widgetInput) => {
|
||||
const widgetId = widgetInput.id ?? v4();
|
||||
|
||||
for (const widgetToRestoreAndUpdate of entitiesToRestoreAndUpdate) {
|
||||
await this.pageLayoutWidgetService.restore(
|
||||
widgetToRestoreAndUpdate.id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
return {
|
||||
id: widgetId,
|
||||
pageLayoutTabId: widgetInput.pageLayoutTabId,
|
||||
title: widgetInput.title,
|
||||
type: widgetInput.type,
|
||||
objectMetadataId: widgetInput.objectMetadataId ?? null,
|
||||
gridPosition: widgetInput.gridPosition,
|
||||
configuration: widgetInput.configuration ?? null,
|
||||
workspaceId,
|
||||
createdAt: now.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
deletedAt: null,
|
||||
universalIdentifier: widgetId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await this.pageLayoutWidgetService.update(
|
||||
widgetToRestoreAndUpdate.id,
|
||||
workspaceId,
|
||||
widgetToRestoreAndUpdate,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
const widgetsToUpdate: FlatPageLayoutWidget[] = entitiesToUpdate.map(
|
||||
(widgetInput) => {
|
||||
const existingWidget = flatPageLayoutWidgetMaps.byId[widgetInput.id];
|
||||
|
||||
for (const widgetToCreate of entitiesToCreate) {
|
||||
await this.pageLayoutWidgetService.create(
|
||||
widgetToCreate as CreatePageLayoutWidgetInput,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
return {
|
||||
...existingWidget!,
|
||||
pageLayoutTabId: widgetInput.pageLayoutTabId,
|
||||
title: widgetInput.title,
|
||||
type: widgetInput.type,
|
||||
objectMetadataId: widgetInput.objectMetadataId ?? null,
|
||||
gridPosition: widgetInput.gridPosition,
|
||||
configuration: widgetInput.configuration ?? null,
|
||||
updatedAt: now.toISOString(),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const widgetsToRestoreAndUpdate: FlatPageLayoutWidget[] =
|
||||
entitiesToRestoreAndUpdate.map((widgetInput) => {
|
||||
const existingWidget = flatPageLayoutWidgetMaps.byId[widgetInput.id];
|
||||
|
||||
return {
|
||||
...existingWidget!,
|
||||
pageLayoutTabId: widgetInput.pageLayoutTabId,
|
||||
title: widgetInput.title,
|
||||
type: widgetInput.type,
|
||||
objectMetadataId: widgetInput.objectMetadataId ?? null,
|
||||
gridPosition: widgetInput.gridPosition,
|
||||
configuration: widgetInput.configuration ?? null,
|
||||
deletedAt: null,
|
||||
updatedAt: now.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
const widgetsToDelete: FlatPageLayoutWidget[] = idsToDelete
|
||||
.map((widgetId) => {
|
||||
const existingWidget = flatPageLayoutWidgetMaps.byId[widgetId];
|
||||
|
||||
if (!isDefined(existingWidget)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...existingWidget,
|
||||
deletedAt: now.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
return {
|
||||
widgetsToCreate,
|
||||
widgetsToUpdate: [
|
||||
...widgetsToUpdate,
|
||||
...widgetsToRestoreAndUpdate,
|
||||
...widgetsToDelete,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+362
-310
@@ -1,83 +1,178 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { EntityManager, IsNull, Repository } from 'typeorm';
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
import { FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util';
|
||||
import { fromDeletePageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-delete-page-layout-widget-input-to-flat-page-layout-widget-or-throw.util';
|
||||
import { fromDestroyPageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-destroy-page-layout-widget-input-to-flat-page-layout-widget-or-throw.util';
|
||||
import { fromRestorePageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-restore-page-layout-widget-input-to-flat-page-layout-widget-or-throw.util';
|
||||
import {
|
||||
fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThrow,
|
||||
type UpdatePageLayoutWidgetInputWithId,
|
||||
} from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util';
|
||||
import { CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout-widget.input';
|
||||
import { UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-widget.input';
|
||||
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-widget.dto';
|
||||
import { WidgetConfigurationInterface } from 'src/engine/metadata-modules/page-layout/dtos/widget-configuration.interface';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-widget.entity';
|
||||
import {
|
||||
PageLayoutTabException,
|
||||
PageLayoutTabExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-tab.exception';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout/enums/widget-type.enum';
|
||||
import {
|
||||
PageLayoutWidgetException,
|
||||
PageLayoutWidgetExceptionCode,
|
||||
PageLayoutWidgetExceptionMessageKey,
|
||||
generatePageLayoutWidgetExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-widget.exception';
|
||||
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout/services/page-layout-tab.service';
|
||||
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
|
||||
import { validateAndTransformWidgetConfiguration } from 'src/engine/metadata-modules/page-layout/utils/validate-and-transform-widget-configuration.util';
|
||||
import { validateWidgetGridPosition } from 'src/engine/metadata-modules/page-layout/utils/validate-widget-grid-position.util';
|
||||
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
type WidgetMigrationOperations = {
|
||||
flatEntityToCreate: FlatPageLayoutWidget[];
|
||||
flatEntityToUpdate: FlatPageLayoutWidget[];
|
||||
flatEntityToDelete: FlatPageLayoutWidget[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PageLayoutWidgetService {
|
||||
constructor(
|
||||
@InjectRepository(PageLayoutWidgetEntity)
|
||||
private readonly pageLayoutWidgetRepository: Repository<PageLayoutWidgetEntity>,
|
||||
private readonly pageLayoutTabService: PageLayoutTabService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
private getPageLayoutWidgetRepository(
|
||||
transactionManager?: EntityManager,
|
||||
): Repository<PageLayoutWidgetEntity> {
|
||||
return transactionManager
|
||||
? transactionManager.getRepository(PageLayoutWidgetEntity)
|
||||
: this.pageLayoutWidgetRepository;
|
||||
private async getFlatPageLayoutWidgetMaps(
|
||||
workspaceId: string,
|
||||
): Promise<FlatPageLayoutWidgetMaps> {
|
||||
const { flatPageLayoutWidgetMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutWidgetMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return flatPageLayoutWidgetMaps;
|
||||
}
|
||||
|
||||
private async validateWidgetConfigurationOrThrow({
|
||||
type,
|
||||
configuration,
|
||||
workspaceId,
|
||||
titleForError,
|
||||
}: {
|
||||
type: WidgetType;
|
||||
configuration: Record<string, unknown>;
|
||||
workspaceId: string;
|
||||
titleForError: string;
|
||||
}): Promise<WidgetConfigurationInterface> {
|
||||
const isDashboardV2Enabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_DASHBOARD_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
let validatedConfig: WidgetConfigurationInterface | null = null;
|
||||
|
||||
try {
|
||||
validatedConfig = await validateAndTransformWidgetConfiguration({
|
||||
type,
|
||||
configuration,
|
||||
isDashboardV2Enabled,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
|
||||
titleForError,
|
||||
type,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(validatedConfig)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
|
||||
titleForError,
|
||||
type,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
return validatedConfig;
|
||||
}
|
||||
|
||||
private async validateAndRunWidgetMigration({
|
||||
workspaceId,
|
||||
operations,
|
||||
errorMessage,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
operations: WidgetMigrationOperations;
|
||||
errorMessage: string;
|
||||
}): Promise<void> {
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutWidget: operations,
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
errorMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async findByPageLayoutTabId(
|
||||
workspaceId: string,
|
||||
pageLayoutTabId: string,
|
||||
transactionManager?: EntityManager,
|
||||
withDeleted = false,
|
||||
): Promise<PageLayoutWidgetEntity[]> {
|
||||
const repository = this.getPageLayoutWidgetRepository(transactionManager);
|
||||
): Promise<PageLayoutWidgetDTO[]> {
|
||||
const flatPageLayoutWidgetMaps =
|
||||
await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
|
||||
return repository.find({
|
||||
where: {
|
||||
pageLayoutTabId,
|
||||
workspaceId,
|
||||
},
|
||||
order: { createdAt: 'ASC' },
|
||||
withDeleted,
|
||||
});
|
||||
return Object.values(flatPageLayoutWidgetMaps.byId)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(widget) =>
|
||||
widget.pageLayoutTabId === pageLayoutTabId &&
|
||||
!isDefined(widget.deletedAt),
|
||||
)
|
||||
.sort(
|
||||
(widgetA, widgetB) =>
|
||||
new Date(widgetA.createdAt).getTime() -
|
||||
new Date(widgetB.createdAt).getTime(),
|
||||
)
|
||||
.map(fromFlatPageLayoutWidgetToPageLayoutWidgetDto);
|
||||
}
|
||||
|
||||
async findByIdOrThrow(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutWidgetEntity> {
|
||||
const repository = this.getPageLayoutWidgetRepository(transactionManager);
|
||||
): Promise<PageLayoutWidgetDTO> {
|
||||
const flatPageLayoutWidgetMaps =
|
||||
await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
|
||||
const pageLayoutWidget = await repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
const flatWidget = flatPageLayoutWidgetMaps.byId[id];
|
||||
|
||||
if (!isDefined(pageLayoutWidget)) {
|
||||
if (!isDefined(flatWidget) || isDefined(flatWidget.deletedAt)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
@@ -87,15 +182,65 @@ export class PageLayoutWidgetService {
|
||||
);
|
||||
}
|
||||
|
||||
return pageLayoutWidget;
|
||||
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(flatWidget);
|
||||
}
|
||||
|
||||
async create(
|
||||
pageLayoutWidgetData: CreatePageLayoutWidgetInput,
|
||||
createPageLayoutWidgetInput: CreatePageLayoutWidgetInput,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutWidgetEntity> {
|
||||
if (!isDefined(pageLayoutWidgetData.title)) {
|
||||
): Promise<PageLayoutWidgetDTO> {
|
||||
this.validateCreateInput(createPageLayoutWidgetInput);
|
||||
|
||||
validateWidgetGridPosition(
|
||||
createPageLayoutWidgetInput.gridPosition,
|
||||
createPageLayoutWidgetInput.title,
|
||||
);
|
||||
|
||||
const validatedConfig = await this.getValidatedConfigurationForCreate(
|
||||
createPageLayoutWidgetInput,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const flatPageLayoutWidgetToCreate =
|
||||
fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate({
|
||||
createPageLayoutWidgetInput: {
|
||||
...createPageLayoutWidgetInput,
|
||||
...(validatedConfig && {
|
||||
configuration: validatedConfig as Record<string, unknown>,
|
||||
}),
|
||||
},
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
await this.validateAndRunWidgetMigration({
|
||||
workspaceId,
|
||||
operations: {
|
||||
flatEntityToCreate: [flatPageLayoutWidgetToCreate],
|
||||
flatEntityToUpdate: [],
|
||||
flatEntityToDelete: [],
|
||||
},
|
||||
errorMessage:
|
||||
'Multiple validation errors occurred while creating page layout widget',
|
||||
});
|
||||
|
||||
const recomputedMaps = await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
|
||||
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: flatPageLayoutWidgetToCreate.id,
|
||||
flatEntityMaps: recomputedMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private validateCreateInput(input: CreatePageLayoutWidgetInput): void {
|
||||
if (!isDefined(input.title)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.TITLE_REQUIRED,
|
||||
@@ -104,7 +249,7 @@ export class PageLayoutWidgetService {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(pageLayoutWidgetData.pageLayoutTabId)) {
|
||||
if (!isDefined(input.pageLayoutTabId)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_ID_REQUIRED,
|
||||
@@ -113,7 +258,7 @@ export class PageLayoutWidgetService {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(pageLayoutWidgetData.gridPosition)) {
|
||||
if (!isDefined(input.gridPosition)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.GRID_POSITION_REQUIRED,
|
||||
@@ -121,131 +266,36 @@ export class PageLayoutWidgetService {
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validateWidgetGridPosition(
|
||||
pageLayoutWidgetData.gridPosition,
|
||||
pageLayoutWidgetData.title,
|
||||
);
|
||||
|
||||
try {
|
||||
const repository = this.getPageLayoutWidgetRepository(transactionManager);
|
||||
|
||||
const pageLayoutTab = await (
|
||||
transactionManager
|
||||
? transactionManager.getRepository(PageLayoutTabEntity)
|
||||
: repository.manager.getRepository(PageLayoutTabEntity)
|
||||
).findOne({
|
||||
where: {
|
||||
id: pageLayoutWidgetData.pageLayoutTabId,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['pageLayout'],
|
||||
});
|
||||
|
||||
if (!isDefined(pageLayoutTab)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
pageLayoutWidgetData.pageLayoutTabId,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
let validatedConfig: WidgetConfigurationInterface | null = null;
|
||||
|
||||
if (pageLayoutWidgetData.configuration && pageLayoutWidgetData.type) {
|
||||
const isDashboardV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_DASHBOARD_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
try {
|
||||
validatedConfig = await validateAndTransformWidgetConfiguration({
|
||||
type: pageLayoutWidgetData.type,
|
||||
configuration: pageLayoutWidgetData.configuration,
|
||||
isDashboardV2Enabled,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
|
||||
pageLayoutWidgetData.title,
|
||||
pageLayoutWidgetData.type,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
if (!validatedConfig) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
|
||||
pageLayoutWidgetData.title,
|
||||
pageLayoutWidgetData.type,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const insertResult = await repository.insert({
|
||||
...pageLayoutWidgetData,
|
||||
workspaceId,
|
||||
universalIdentifier: v4(),
|
||||
applicationId: pageLayoutTab.pageLayout.applicationId,
|
||||
...(validatedConfig && { configuration: validatedConfig }),
|
||||
} as QueryDeepPartialEntity<PageLayoutWidgetEntity>);
|
||||
|
||||
return this.findByIdOrThrow(
|
||||
insertResult.identifiers[0].id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof PageLayoutTabException &&
|
||||
error.code === PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND
|
||||
) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
private async getValidatedConfigurationForCreate(
|
||||
input: CreatePageLayoutWidgetInput,
|
||||
workspaceId: string,
|
||||
): Promise<WidgetConfigurationInterface | null> {
|
||||
if (!input.configuration || !input.type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.validateWidgetConfigurationOrThrow({
|
||||
type: input.type,
|
||||
configuration: input.configuration,
|
||||
workspaceId,
|
||||
titleForError: input.title,
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
updateData: UpdatePageLayoutWidgetInput,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutWidgetEntity> {
|
||||
const repository = this.getPageLayoutWidgetRepository(transactionManager);
|
||||
): Promise<PageLayoutWidgetDTO> {
|
||||
const existingFlatPageLayoutWidgetMaps =
|
||||
await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
|
||||
const existingWidget = await repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(existingWidget)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
const existingWidget = this.getExistingWidgetOrThrow(
|
||||
id,
|
||||
existingFlatPageLayoutWidgetMaps,
|
||||
);
|
||||
|
||||
if (updateData.gridPosition) {
|
||||
const titleForValidation = updateData.title ?? existingWidget.title;
|
||||
@@ -253,92 +303,56 @@ export class PageLayoutWidgetService {
|
||||
validateWidgetGridPosition(updateData.gridPosition, titleForValidation);
|
||||
}
|
||||
|
||||
let validatedConfig: WidgetConfigurationInterface | null = null;
|
||||
|
||||
if (updateData.configuration) {
|
||||
const typeForValidation = updateData.type ?? existingWidget.type;
|
||||
const titleForError = updateData.title ?? existingWidget.title;
|
||||
|
||||
if (typeForValidation) {
|
||||
const isDashboardV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_DASHBOARD_V2_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
try {
|
||||
validatedConfig = await validateAndTransformWidgetConfiguration({
|
||||
type: typeForValidation,
|
||||
configuration: updateData.configuration,
|
||||
isDashboardV2Enabled,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
|
||||
titleForError,
|
||||
typeForValidation,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
if (!validatedConfig) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.INVALID_WIDGET_CONFIGURATION,
|
||||
titleForError,
|
||||
typeForValidation,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await repository.update({ id }, {
|
||||
...updateData,
|
||||
...(validatedConfig && { configuration: validatedConfig }),
|
||||
} as QueryDeepPartialEntity<PageLayoutWidgetEntity>);
|
||||
|
||||
return this.findByIdOrThrow(id, workspaceId, transactionManager);
|
||||
}
|
||||
|
||||
async delete(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutWidgetEntity> {
|
||||
const pageLayoutWidget = await this.findByIdOrThrow(
|
||||
id,
|
||||
const validatedConfig = await this.getValidatedConfigurationForUpdate(
|
||||
updateData,
|
||||
existingWidget,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const repository = this.getPageLayoutWidgetRepository(transactionManager);
|
||||
|
||||
await repository.softDelete(id);
|
||||
|
||||
return pageLayoutWidget;
|
||||
}
|
||||
|
||||
async destroy(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<boolean> {
|
||||
const repository = this.getPageLayoutWidgetRepository(transactionManager);
|
||||
|
||||
const pageLayoutWidget = await repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
const updatePageLayoutWidgetInput: UpdatePageLayoutWidgetInputWithId = {
|
||||
id,
|
||||
update: {
|
||||
...updateData,
|
||||
...(validatedConfig && {
|
||||
configuration: validatedConfig as Record<string, unknown>,
|
||||
}),
|
||||
},
|
||||
withDeleted: true,
|
||||
};
|
||||
|
||||
const flatPageLayoutWidgetToUpdate =
|
||||
fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThrow({
|
||||
updatePageLayoutWidgetInput,
|
||||
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
|
||||
});
|
||||
|
||||
await this.validateAndRunWidgetMigration({
|
||||
workspaceId,
|
||||
operations: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToUpdate: [flatPageLayoutWidgetToUpdate],
|
||||
flatEntityToDelete: [],
|
||||
},
|
||||
errorMessage:
|
||||
'Multiple validation errors occurred while updating page layout widget',
|
||||
});
|
||||
|
||||
if (!isDefined(pageLayoutWidget)) {
|
||||
const recomputedMaps = await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
|
||||
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getExistingWidgetOrThrow(
|
||||
id: string,
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps,
|
||||
): FlatPageLayoutWidget {
|
||||
const existingWidget = flatPageLayoutWidgetMaps.byId[id];
|
||||
|
||||
if (!isDefined(existingWidget) || isDefined(existingWidget.deletedAt)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
@@ -348,79 +362,117 @@ export class PageLayoutWidgetService {
|
||||
);
|
||||
}
|
||||
|
||||
await repository.delete(id);
|
||||
return existingWidget;
|
||||
}
|
||||
|
||||
private async getValidatedConfigurationForUpdate(
|
||||
updateData: UpdatePageLayoutWidgetInput,
|
||||
existingWidget: FlatPageLayoutWidget,
|
||||
workspaceId: string,
|
||||
): Promise<WidgetConfigurationInterface | null> {
|
||||
if (!updateData.configuration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typeForValidation = updateData.type ?? existingWidget.type;
|
||||
|
||||
if (!typeForValidation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const titleForError = updateData.title ?? existingWidget.title;
|
||||
|
||||
return this.validateWidgetConfigurationOrThrow({
|
||||
type: typeForValidation,
|
||||
configuration: updateData.configuration,
|
||||
workspaceId,
|
||||
titleForError,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<PageLayoutWidgetDTO> {
|
||||
const existingFlatPageLayoutWidgetMaps =
|
||||
await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
|
||||
const flatPageLayoutWidgetToDelete =
|
||||
fromDeletePageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow({
|
||||
deletePageLayoutWidgetInput: { id },
|
||||
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
|
||||
});
|
||||
|
||||
await this.validateAndRunWidgetMigration({
|
||||
workspaceId,
|
||||
operations: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToUpdate: [flatPageLayoutWidgetToDelete],
|
||||
flatEntityToDelete: [],
|
||||
},
|
||||
errorMessage:
|
||||
'Multiple validation errors occurred while deleting page layout widget',
|
||||
});
|
||||
|
||||
const recomputedMaps = await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
|
||||
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async destroy(id: string, workspaceId: string): Promise<boolean> {
|
||||
const existingFlatPageLayoutWidgetMaps =
|
||||
await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
|
||||
const flatPageLayoutWidgetToDestroy =
|
||||
fromDestroyPageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow({
|
||||
destroyPageLayoutWidgetInput: { id },
|
||||
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
|
||||
});
|
||||
|
||||
await this.validateAndRunWidgetMigration({
|
||||
workspaceId,
|
||||
operations: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToUpdate: [],
|
||||
flatEntityToDelete: [flatPageLayoutWidgetToDestroy],
|
||||
},
|
||||
errorMessage:
|
||||
'Multiple validation errors occurred while destroying page layout widget',
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async restore(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutWidgetEntity> {
|
||||
const repository = this.getPageLayoutWidgetRepository(transactionManager);
|
||||
async restore(id: string, workspaceId: string): Promise<PageLayoutWidgetDTO> {
|
||||
const existingFlatPageLayoutWidgetMaps =
|
||||
await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
|
||||
const pageLayoutWidget = await repository.findOne({
|
||||
select: {
|
||||
id: true,
|
||||
deletedAt: true,
|
||||
pageLayoutTabId: true,
|
||||
const flatPageLayoutWidgetToRestore =
|
||||
fromRestorePageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow({
|
||||
restorePageLayoutWidgetInput: { id },
|
||||
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
|
||||
});
|
||||
|
||||
await this.validateAndRunWidgetMigration({
|
||||
workspaceId,
|
||||
operations: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToUpdate: [flatPageLayoutWidgetToRestore],
|
||||
flatEntityToDelete: [],
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
withDeleted: true,
|
||||
errorMessage:
|
||||
'Multiple validation errors occurred while restoring page layout widget',
|
||||
});
|
||||
|
||||
if (!isDefined(pageLayoutWidget)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.PAGE_LAYOUT_WIDGET_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
const recomputedMaps = await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
|
||||
if (!isDefined(pageLayoutWidget.deletedAt)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_DELETED,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.pageLayoutTabService.findByIdOrThrow(
|
||||
pageLayoutWidget.pageLayoutTabId,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof PageLayoutTabException &&
|
||||
error.code === PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND
|
||||
) {
|
||||
throw new PageLayoutWidgetException(
|
||||
generatePageLayoutWidgetExceptionMessage(
|
||||
PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
),
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
await repository.restore(id);
|
||||
|
||||
const restoredPageLayoutWidget = await this.findByIdOrThrow(
|
||||
id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedMaps,
|
||||
}),
|
||||
);
|
||||
|
||||
return restoredPageLayoutWidget;
|
||||
}
|
||||
}
|
||||
|
||||
+331
-145
@@ -1,14 +1,26 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { EntityManager, IsNull, Repository } from 'typeorm';
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
|
||||
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout-maps.type';
|
||||
import { fromCreatePageLayoutInputToFlatPageLayoutToCreate } from 'src/engine/metadata-modules/flat-page-layout/utils/from-create-page-layout-input-to-flat-page-layout-to-create.util';
|
||||
import { fromDeletePageLayoutInputToFlatPageLayoutOrThrow } from 'src/engine/metadata-modules/flat-page-layout/utils/from-delete-page-layout-input-to-flat-page-layout-or-throw.util';
|
||||
import { fromDestroyPageLayoutInputToFlatPageLayoutOrThrow } from 'src/engine/metadata-modules/flat-page-layout/utils/from-destroy-page-layout-input-to-flat-page-layout-or-throw.util';
|
||||
import { fromRestorePageLayoutInputToFlatPageLayoutOrThrow } from 'src/engine/metadata-modules/flat-page-layout/utils/from-restore-page-layout-input-to-flat-page-layout-or-throw.util';
|
||||
import {
|
||||
fromUpdatePageLayoutInputToFlatPageLayoutToUpdateOrThrow,
|
||||
type UpdatePageLayoutInputWithId,
|
||||
} from 'src/engine/metadata-modules/flat-page-layout/utils/from-update-page-layout-input-to-flat-page-layout-to-update-or-throw.util';
|
||||
import { reconstructFlatPageLayoutWithTabsAndWidgets } from 'src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util';
|
||||
import { CreatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout.input';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { UpdatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout.input';
|
||||
import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import {
|
||||
PageLayoutException,
|
||||
@@ -16,77 +28,87 @@ import {
|
||||
PageLayoutExceptionMessageKey,
|
||||
generatePageLayoutExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
|
||||
import { fromFlatPageLayoutToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-to-page-layout-dto.util';
|
||||
import { fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.util';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@Injectable()
|
||||
export class PageLayoutService {
|
||||
private readonly logger = new Logger(PageLayoutService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(PageLayoutEntity)
|
||||
private readonly pageLayoutRepository: Repository<PageLayoutEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
private getPageLayoutRepository(
|
||||
transactionManager?: EntityManager,
|
||||
): Repository<PageLayoutEntity> {
|
||||
return transactionManager
|
||||
? transactionManager.getRepository(PageLayoutEntity)
|
||||
: this.pageLayoutRepository;
|
||||
}
|
||||
async findByWorkspaceId(workspaceId: string): Promise<PageLayoutDTO[]> {
|
||||
const {
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
} = await this.getPageLayoutFlatEntityMaps(workspaceId);
|
||||
|
||||
async findByWorkspaceId(
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutEntity[]> {
|
||||
const repository = this.getPageLayoutRepository(transactionManager);
|
||||
const activeLayouts = Object.values(flatPageLayoutMaps.byId)
|
||||
.filter(isDefined)
|
||||
.filter((layout) => !isDefined(layout.deletedAt));
|
||||
|
||||
return repository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['tabs', 'tabs.widgets'],
|
||||
});
|
||||
return activeLayouts.map((layout) =>
|
||||
fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto(
|
||||
reconstructFlatPageLayoutWithTabsAndWidgets({
|
||||
layout,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async findByObjectMetadataId(
|
||||
workspaceId: string,
|
||||
objectMetadataId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutEntity[]> {
|
||||
const repository = this.getPageLayoutRepository(transactionManager);
|
||||
): Promise<PageLayoutDTO[]> {
|
||||
const {
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
} = await this.getPageLayoutFlatEntityMaps(workspaceId);
|
||||
|
||||
return repository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['tabs', 'tabs.widgets'],
|
||||
});
|
||||
const activeLayouts = Object.values(flatPageLayoutMaps.byId)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(layout) =>
|
||||
layout.objectMetadataId === objectMetadataId &&
|
||||
!isDefined(layout.deletedAt),
|
||||
);
|
||||
|
||||
return activeLayouts.map((layout) =>
|
||||
fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto(
|
||||
reconstructFlatPageLayoutWithTabsAndWidgets({
|
||||
layout,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async findByIdOrThrow(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutEntity> {
|
||||
const repository = this.getPageLayoutRepository(transactionManager);
|
||||
): Promise<PageLayoutDTO> {
|
||||
const {
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
} = await this.getPageLayoutFlatEntityMaps(workspaceId);
|
||||
|
||||
const pageLayout = await repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['tabs', 'tabs.widgets'],
|
||||
});
|
||||
const flatLayout = flatPageLayoutMaps.byId[id];
|
||||
|
||||
if (!isDefined(pageLayout)) {
|
||||
if (!isDefined(flatLayout) || isDefined(flatLayout.deletedAt)) {
|
||||
throw new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
@@ -96,15 +118,37 @@ export class PageLayoutService {
|
||||
);
|
||||
}
|
||||
|
||||
return pageLayout;
|
||||
return fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto(
|
||||
reconstructFlatPageLayoutWithTabsAndWidgets({
|
||||
layout: flatLayout,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async getPageLayoutFlatEntityMaps(workspaceId: string): Promise<{
|
||||
flatPageLayoutMaps: FlatPageLayoutMaps;
|
||||
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
}> {
|
||||
return this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async create(
|
||||
pageLayoutData: CreatePageLayoutInput,
|
||||
createPageLayoutInput: CreatePageLayoutInput,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutEntity> {
|
||||
if (!isDefined(pageLayoutData.name)) {
|
||||
): Promise<Omit<PageLayoutDTO, 'tabs'>> {
|
||||
if (!isNonEmptyString(createPageLayoutInput.name)) {
|
||||
throw new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.NAME_REQUIRED,
|
||||
@@ -113,96 +157,219 @@ export class PageLayoutService {
|
||||
);
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOneOrFail({
|
||||
where: { id: workspaceId },
|
||||
select: ['workspaceCustomApplicationId'],
|
||||
});
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const repository = this.getPageLayoutRepository(transactionManager);
|
||||
const flatPageLayoutToCreate =
|
||||
fromCreatePageLayoutInputToFlatPageLayoutToCreate({
|
||||
createPageLayoutInput,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
const insertResult = await repository.insert({
|
||||
...pageLayoutData,
|
||||
workspaceId,
|
||||
universalIdentifier: v4(),
|
||||
applicationId: workspace.workspaceCustomApplicationId,
|
||||
});
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: [flatPageLayoutToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
return this.findByIdOrThrow(
|
||||
insertResult.identifiers[0].id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while creating page layout',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatPageLayoutMaps: recomputedFlatPageLayoutMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatPageLayoutToPageLayoutDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: flatPageLayoutToCreate.id,
|
||||
flatEntityMaps: recomputedFlatPageLayoutMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
updateData: QueryDeepPartialEntity<PageLayoutEntity>,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutEntity> {
|
||||
const repository = this.getPageLayoutRepository(transactionManager);
|
||||
updateData: UpdatePageLayoutInput,
|
||||
): Promise<Omit<PageLayoutDTO, 'tabs'>> {
|
||||
const { flatPageLayoutMaps: existingFlatPageLayoutMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
await repository.update({ id, workspaceId }, updateData);
|
||||
|
||||
const updatedPageLayout = await this.findByIdOrThrow(
|
||||
const updatePageLayoutInput: UpdatePageLayoutInputWithId = {
|
||||
id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
update: updateData,
|
||||
};
|
||||
|
||||
return updatedPageLayout;
|
||||
const flatPageLayoutToUpdate =
|
||||
fromUpdatePageLayoutInputToFlatPageLayoutToUpdateOrThrow({
|
||||
updatePageLayoutInput,
|
||||
flatPageLayoutMaps: existingFlatPageLayoutMaps,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatPageLayoutToUpdate],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while updating page layout',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatPageLayoutMaps: recomputedFlatPageLayoutMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatPageLayoutToPageLayoutDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedFlatPageLayoutMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async delete(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutEntity> {
|
||||
const pageLayout = await this.findByIdOrThrow(
|
||||
id,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
): Promise<Omit<PageLayoutDTO, 'tabs'>> {
|
||||
const { flatPageLayoutMaps: existingFlatPageLayoutMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatPageLayoutToDelete =
|
||||
fromDeletePageLayoutInputToFlatPageLayoutOrThrow({
|
||||
deletePageLayoutInput: { id },
|
||||
flatPageLayoutMaps: existingFlatPageLayoutMaps,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatPageLayoutToDelete],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while deleting page layout',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatPageLayoutMaps: recomputedFlatPageLayoutMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatPageLayoutToPageLayoutDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedFlatPageLayoutMaps,
|
||||
}),
|
||||
);
|
||||
|
||||
const repository = this.getPageLayoutRepository(transactionManager);
|
||||
|
||||
await repository.softDelete(id);
|
||||
|
||||
return pageLayout;
|
||||
}
|
||||
|
||||
async destroy(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PageLayoutEntity> {
|
||||
const repository = this.getPageLayoutRepository(transactionManager);
|
||||
): Promise<Omit<PageLayoutDTO, 'tabs'>> {
|
||||
const { flatPageLayoutMaps: existingFlatPageLayoutMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const pageLayout = await repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
const flatPageLayoutToDestroy =
|
||||
fromDestroyPageLayoutInputToFlatPageLayoutOrThrow({
|
||||
destroyPageLayoutInput: { id },
|
||||
flatPageLayoutMaps: existingFlatPageLayoutMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(pageLayout)) {
|
||||
throw new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [flatPageLayoutToDestroy],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while destroying page layout',
|
||||
);
|
||||
}
|
||||
|
||||
if (pageLayout.type === PageLayoutType.DASHBOARD) {
|
||||
if (flatPageLayoutToDestroy.type === PageLayoutType.DASHBOARD) {
|
||||
await this.destroyAssociatedDashboards(id, workspaceId);
|
||||
}
|
||||
|
||||
await repository.delete(id);
|
||||
|
||||
return pageLayout;
|
||||
return fromFlatPageLayoutToPageLayoutDto(flatPageLayoutToDestroy);
|
||||
}
|
||||
|
||||
private async destroyAssociatedDashboards(
|
||||
@@ -233,40 +400,59 @@ export class PageLayoutService {
|
||||
}
|
||||
}
|
||||
|
||||
async restore(id: string, workspaceId: string): Promise<PageLayoutEntity> {
|
||||
const pageLayout = await this.pageLayoutRepository.findOne({
|
||||
select: {
|
||||
id: true,
|
||||
deletedAt: true,
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
async restore(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<Omit<PageLayoutDTO, 'tabs'>> {
|
||||
const { flatPageLayoutMaps: existingFlatPageLayoutMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(pageLayout)) {
|
||||
throw new PageLayoutException(
|
||||
generatePageLayoutExceptionMessage(
|
||||
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND,
|
||||
const flatPageLayoutToRestore =
|
||||
fromRestorePageLayoutInputToFlatPageLayoutOrThrow({
|
||||
restorePageLayoutInput: { id },
|
||||
flatPageLayoutMaps: existingFlatPageLayoutMaps,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatPageLayoutToRestore],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while restoring page layout',
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(pageLayout.deletedAt)) {
|
||||
throw new PageLayoutException(
|
||||
'Page layout is not deleted and cannot be restored',
|
||||
PageLayoutExceptionCode.INVALID_PAGE_LAYOUT_DATA,
|
||||
const { flatPageLayoutMaps: recomputedFlatPageLayoutMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutMaps'],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await this.pageLayoutRepository.restore(id);
|
||||
|
||||
const restoredPageLayout = await this.findByIdOrThrow(id, workspaceId);
|
||||
|
||||
return restoredPageLayout;
|
||||
return fromFlatPageLayoutToPageLayoutDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedFlatPageLayoutMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-tab.dto';
|
||||
|
||||
export const fromFlatPageLayoutTabToPageLayoutTabDto = (
|
||||
flatPageLayoutTab: FlatPageLayoutTab,
|
||||
): Omit<PageLayoutTabDTO, 'widgets'> => {
|
||||
const {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
widgetIds: _widgetIds,
|
||||
...rest
|
||||
} = flatPageLayoutTab;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
createdAt: new Date(createdAt),
|
||||
updatedAt: new Date(updatedAt),
|
||||
deletedAt: deletedAt ? new Date(deletedAt) : null,
|
||||
};
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { type FlatPageLayoutTabWithWidgets } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/reconstruct-flat-page-layout-tab-with-widgets.util';
|
||||
import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-tab.dto';
|
||||
import { fromFlatPageLayoutTabToPageLayoutTabDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-tab-to-page-layout-tab-dto.util';
|
||||
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
|
||||
|
||||
export const fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto = (
|
||||
flatPageLayoutTabWithWidgets: FlatPageLayoutTabWithWidgets,
|
||||
): PageLayoutTabDTO => {
|
||||
const { widgets, ...flatPageLayoutTab } = flatPageLayoutTabWithWidgets;
|
||||
|
||||
return {
|
||||
...fromFlatPageLayoutTabToPageLayoutTabDto(flatPageLayoutTab),
|
||||
widgets: widgets.map(fromFlatPageLayoutWidgetToPageLayoutWidgetDto),
|
||||
};
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
|
||||
import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto';
|
||||
|
||||
export const fromFlatPageLayoutToPageLayoutDto = (
|
||||
flatPageLayout: FlatPageLayout,
|
||||
): Omit<PageLayoutDTO, 'tabs'> => {
|
||||
const {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
tabIds: _tabIds,
|
||||
...rest
|
||||
} = flatPageLayout;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
createdAt: new Date(createdAt),
|
||||
updatedAt: new Date(updatedAt),
|
||||
deletedAt: deletedAt ? new Date(deletedAt) : null,
|
||||
};
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-widget.dto';
|
||||
|
||||
export const fromFlatPageLayoutWidgetToPageLayoutWidgetDto = (
|
||||
flatPageLayoutWidget: FlatPageLayoutWidget,
|
||||
): PageLayoutWidgetDTO => {
|
||||
const { createdAt, updatedAt, deletedAt, ...rest } = flatPageLayoutWidget;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
createdAt: new Date(createdAt),
|
||||
updatedAt: new Date(updatedAt),
|
||||
deletedAt: deletedAt ? new Date(deletedAt) : null,
|
||||
};
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { type FlatPageLayoutWithTabsAndWidgets } from 'src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util';
|
||||
import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto';
|
||||
import { fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-tab-with-widgets-to-page-layout-tab-dto.util';
|
||||
import { fromFlatPageLayoutToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-to-page-layout-dto.util';
|
||||
|
||||
export const fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto = (
|
||||
flatPageLayoutWithTabsAndWidgets: FlatPageLayoutWithTabsAndWidgets,
|
||||
): PageLayoutDTO => {
|
||||
const { tabs, ...flatPageLayout } = flatPageLayoutWithTabsAndWidgets;
|
||||
|
||||
return {
|
||||
...fromFlatPageLayoutToPageLayoutDto(flatPageLayout),
|
||||
tabs: tabs.map(fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto),
|
||||
};
|
||||
};
|
||||
+11
-1
@@ -1,3 +1,4 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
@@ -16,8 +17,17 @@ import {
|
||||
PageLayoutException,
|
||||
PageLayoutExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
|
||||
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
import { workspaceMigrationBuilderExceptionV2Formatter } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2-formatter';
|
||||
|
||||
export const pageLayoutGraphqlApiExceptionHandler = (
|
||||
error: Error,
|
||||
i18n: I18n,
|
||||
) => {
|
||||
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
|
||||
return workspaceMigrationBuilderExceptionV2Formatter(error, i18n);
|
||||
}
|
||||
|
||||
export const pageLayoutGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof PageLayoutException) {
|
||||
switch (error.code) {
|
||||
case PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND:
|
||||
|
||||
+30
-7
@@ -1,20 +1,43 @@
|
||||
import { ArgumentsHost, Catch } from '@nestjs/common';
|
||||
import { GqlExceptionFilter } from '@nestjs/graphql';
|
||||
import {
|
||||
Catch,
|
||||
type ExceptionFilter,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { PageLayoutTabException } from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-tab.exception';
|
||||
import { PageLayoutWidgetException } from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-widget.exception';
|
||||
import { PageLayoutException } from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
|
||||
import { pageLayoutGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception-handler.util';
|
||||
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
|
||||
@Catch(
|
||||
PageLayoutException,
|
||||
PageLayoutTabException,
|
||||
PageLayoutWidgetException,
|
||||
WorkspaceMigrationBuilderExceptionV2,
|
||||
)
|
||||
@Injectable()
|
||||
export class PageLayoutGraphqlApiExceptionFilter implements ExceptionFilter {
|
||||
constructor(private readonly i18nService: I18nService) {}
|
||||
|
||||
@Catch(PageLayoutException, PageLayoutTabException, PageLayoutWidgetException)
|
||||
export class PageLayoutGraphqlApiExceptionFilter implements GqlExceptionFilter {
|
||||
catch(
|
||||
exception:
|
||||
| PageLayoutException
|
||||
| PageLayoutTabException
|
||||
| PageLayoutWidgetException,
|
||||
_host: ArgumentsHost,
|
||||
| PageLayoutWidgetException
|
||||
| WorkspaceMigrationBuilderExceptionV2,
|
||||
host: ExecutionContext,
|
||||
) {
|
||||
return pageLayoutGraphqlApiExceptionHandler(exception);
|
||||
const gqlContext = GqlExecutionContext.create(host);
|
||||
const ctx = gqlContext.getContext();
|
||||
const userLocale = ctx.req?.locale ?? SOURCE_LOCALE;
|
||||
const i18n = this.i18nService.getI18nInstance(userLocale);
|
||||
|
||||
return pageLayoutGraphqlApiExceptionHandler(exception, i18n);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user