diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts index 1ecaf4668fe..3891751123c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts @@ -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 { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util.ts new file mode 100644 index 00000000000..e442c67ccfc --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util.ts @@ -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: [], + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-delete-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-delete-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util.ts new file mode 100644 index 00000000000..0f4caa77f09 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-delete-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util.ts @@ -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(), + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-destroy-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-destroy-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util.ts new file mode 100644 index 00000000000..d7b7c76310d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-destroy-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util.ts @@ -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; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-restore-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-restore-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util.ts new file mode 100644 index 00000000000..a519fe64259 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-restore-page-layout-tab-input-to-flat-page-layout-tab-or-throw.util.ts @@ -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, + }; +}; diff --git a/packages/twenty-server/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.ts b/packages/twenty-server/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.ts new file mode 100644 index 00000000000..b981ff7c337 --- /dev/null +++ b/packages/twenty-server/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.ts @@ -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, + }); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/reconstruct-flat-page-layout-tab-with-widgets.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/reconstruct-flat-page-layout-tab-with-widgets.util.ts new file mode 100644 index 00000000000..befdef9cb4f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/reconstruct-flat-page-layout-tab-with-widgets.util.ts @@ -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), + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-restore-page-layout-widget-input-to-flat-page-layout-widget-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-restore-page-layout-widget-input-to-flat-page-layout-widget-or-throw.util.ts new file mode 100644 index 00000000000..e99ab59aa8e --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-restore-page-layout-widget-input-to-flat-page-layout-widget-or-throw.util.ts @@ -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, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/constants/flat-page-layout-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/constants/flat-page-layout-editable-properties.constant.ts index 9ce02671ba8..45303f1c5ad 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/constants/flat-page-layout-editable-properties.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/constants/flat-page-layout-editable-properties.constant.ts @@ -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']; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/flat-page-layout.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/flat-page-layout.module.ts index c070aff5ba9..cc149ba8f32 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/flat-page-layout.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/flat-page-layout.module.ts @@ -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], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/index.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/index.ts new file mode 100644 index 00000000000..2d5ef666072 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/index.ts @@ -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'; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-create-page-layout-input-to-flat-page-layout-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-create-page-layout-input-to-flat-page-layout-to-create.util.ts new file mode 100644 index 00000000000..1630725e56c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-create-page-layout-input-to-flat-page-layout-to-create.util.ts @@ -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: [], + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-delete-page-layout-input-to-flat-page-layout-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-delete-page-layout-input-to-flat-page-layout-or-throw.util.ts new file mode 100644 index 00000000000..db0449a1391 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-delete-page-layout-input-to-flat-page-layout-or-throw.util.ts @@ -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(), + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-destroy-page-layout-input-to-flat-page-layout-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-destroy-page-layout-input-to-flat-page-layout-or-throw.util.ts new file mode 100644 index 00000000000..dbe8029fdd9 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-destroy-page-layout-input-to-flat-page-layout-or-throw.util.ts @@ -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; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-restore-page-layout-input-to-flat-page-layout-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-restore-page-layout-input-to-flat-page-layout-or-throw.util.ts new file mode 100644 index 00000000000..6939c4aa49a --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-restore-page-layout-input-to-flat-page-layout-or-throw.util.ts @@ -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, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-update-page-layout-input-to-flat-page-layout-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-update-page-layout-input-to-flat-page-layout-to-update-or-throw.util.ts new file mode 100644 index 00000000000..20ffc1c56a5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/from-update-page-layout-input-to-flat-page-layout-to-update-or-throw.util.ts @@ -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, + }); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util.ts new file mode 100644 index 00000000000..da843627290 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util.ts @@ -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), + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/controllers/page-layout.controller.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/controllers/page-layout.controller.ts index 20dca13f91e..c43697f37de 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/controllers/page-layout.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/controllers/page-layout.controller.ts @@ -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 { + ): Promise { const deletedPageLayout = await this.pageLayoutService.delete( id, workspace.id, diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/page-layout.module.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/page-layout.module.ts index 2391a56c52e..a0a0366f5a6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/page-layout.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/page-layout.module.ts @@ -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], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout-tab.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout-tab.resolver.ts index eeebb150256..f78afba0416 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout-tab.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout-tab.resolver.ts @@ -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) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout-widget.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout-widget.resolver.ts index fda6826e020..d189fe1eaaa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout-widget.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout-widget.resolver.ts @@ -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) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout.resolver.ts index cc7f659ec6d..4da8e8aa785 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout.resolver.ts @@ -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) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/__tests__/page-layout-tab.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/__tests__/page-layout-tab.service.spec.ts deleted file mode 100644 index d86c1375fb7..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/__tests__/page-layout-tab.service.spec.ts +++ /dev/null @@ -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; - 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); - pageLayoutTabRepository = module.get>( - getRepositoryToken(PageLayoutTabEntity), - ); - pageLayoutService = module.get(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, - ); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/__tests__/page-layout-update.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/__tests__/page-layout-update.service.spec.ts deleted file mode 100644 index 8f1aa513340..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/__tests__/page-layout-update.service.spec.ts +++ /dev/null @@ -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, - ); - pageLayoutService = module.get(PageLayoutService); - pageLayoutTabService = - module.get(PageLayoutTabService); - pageLayoutWidgetService = module.get( - 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); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/__tests__/page-layout-widget.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/__tests__/page-layout-widget.service.spec.ts deleted file mode 100644 index ee05fc44040..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/__tests__/page-layout-widget.service.spec.ts +++ /dev/null @@ -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; - 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, - ); - pageLayoutWidgetRepository = module.get>( - getRepositoryToken(PageLayoutWidgetEntity), - ); - pageLayoutTabService = - module.get(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, - ); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/__tests__/page-layout.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/__tests__/page-layout.service.spec.ts deleted file mode 100644 index 32d6910ea6c..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/__tests__/page-layout.service.spec.ts +++ /dev/null @@ -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; - let workspaceRepository: Repository; - 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); - pageLayoutRepository = module.get>( - getRepositoryToken(PageLayoutEntity), - ); - workspaceRepository = module.get>( - getRepositoryToken(WorkspaceEntity), - ); - twentyORMGlobalManager = module.get( - 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, - ), - ); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-tab.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-tab.service.ts index 8bf2476077a..f76dbfcf487 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-tab.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-tab.service.ts @@ -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, - private readonly pageLayoutService: PageLayoutService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + private readonly applicationService: ApplicationService, ) {} - private getPageLayoutTabRepository( - transactionManager?: EntityManager, - ): Repository { - return transactionManager - ? transactionManager.getRepository(PageLayoutTabEntity) - : this.pageLayoutTabRepository; - } - async findByPageLayoutId( workspaceId: string, pageLayoutId: string, - transactionManager?: EntityManager, - withDeleted = false, - ): Promise { - const repository = this.getPageLayoutTabRepository(transactionManager); + ): Promise { + 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 { - const repository = this.getPageLayoutTabRepository(transactionManager); + ): Promise { + 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 { - if (!isDefined(pageLayoutTabData.title)) { + ): Promise> { + 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, - transactionManager?: EntityManager, - ): Promise { - const repository = this.getPageLayoutTabRepository(transactionManager); + updateData: UpdatePageLayoutTabInput, + ): Promise> { + 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 { - const pageLayoutTab = await this.findByIdOrThrow( - id, - workspaceId, - transactionManager, - ); + ): Promise> { + 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 { - 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 { + 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 { - const repository = this.getPageLayoutTabRepository(transactionManager); + ): Promise> { + 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; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts index 68ea76f99ea..a75b4cad1c8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts @@ -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 { - if (!isDefined(transactionManager)) { - const queryRunner = this.dataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const result = await this.updatePageLayoutWithTabsWithinTransaction({ - id, + }: UpdatePageLayoutWithTabsParams): Promise { + 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 { - 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 { - 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 { - 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, + ], + }; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-widget.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-widget.service.ts index 7ddecd6403e..c548e307951 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-widget.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-widget.service.ts @@ -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, - 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 { - return transactionManager - ? transactionManager.getRepository(PageLayoutWidgetEntity) - : this.pageLayoutWidgetRepository; + private async getFlatPageLayoutWidgetMaps( + workspaceId: string, + ): Promise { + const { flatPageLayoutWidgetMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatPageLayoutWidgetMaps'], + }, + ); + + return flatPageLayoutWidgetMaps; + } + + private async validateWidgetConfigurationOrThrow({ + type, + configuration, + workspaceId, + titleForError, + }: { + type: WidgetType; + configuration: Record; + workspaceId: string; + titleForError: string; + }): Promise { + 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 { + 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 { - const repository = this.getPageLayoutWidgetRepository(transactionManager); + ): Promise { + 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 { - const repository = this.getPageLayoutWidgetRepository(transactionManager); + ): Promise { + 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 { - if (!isDefined(pageLayoutWidgetData.title)) { + ): Promise { + 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, + }), + }, + 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); - - 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 { + 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 { - const repository = this.getPageLayoutWidgetRepository(transactionManager); + ): Promise { + 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); - - return this.findByIdOrThrow(id, workspaceId, transactionManager); - } - - async delete( - id: string, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise { - 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 { - const repository = this.getPageLayoutWidgetRepository(transactionManager); - - const pageLayoutWidget = await repository.findOne({ - where: { - id, - workspaceId, + const updatePageLayoutWidgetInput: UpdatePageLayoutWidgetInputWithId = { + id, + update: { + ...updateData, + ...(validatedConfig && { + configuration: validatedConfig as Record, + }), }, - 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 { + 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 { + 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 { + 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 { - const repository = this.getPageLayoutWidgetRepository(transactionManager); + async restore(id: string, workspaceId: string): Promise { + 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; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout.service.ts index 2405554c9f6..5cbfb8a53fe 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout.service.ts @@ -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, - @InjectRepository(WorkspaceEntity) - private readonly workspaceRepository: Repository, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + private readonly applicationService: ApplicationService, ) {} - private getPageLayoutRepository( - transactionManager?: EntityManager, - ): Repository { - return transactionManager - ? transactionManager.getRepository(PageLayoutEntity) - : this.pageLayoutRepository; - } + async findByWorkspaceId(workspaceId: string): Promise { + const { + flatPageLayoutMaps, + flatPageLayoutTabMaps, + flatPageLayoutWidgetMaps, + } = await this.getPageLayoutFlatEntityMaps(workspaceId); - async findByWorkspaceId( - workspaceId: string, - transactionManager?: EntityManager, - ): Promise { - 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 { - const repository = this.getPageLayoutRepository(transactionManager); + ): Promise { + 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 { - const repository = this.getPageLayoutRepository(transactionManager); + ): Promise { + 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 { - if (!isDefined(pageLayoutData.name)) { + ): Promise> { + 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, - transactionManager?: EntityManager, - ): Promise { - const repository = this.getPageLayoutRepository(transactionManager); + updateData: UpdatePageLayoutInput, + ): Promise> { + 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 { - const pageLayout = await this.findByIdOrThrow( - id, - workspaceId, - transactionManager, + ): Promise> { + 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 { - const repository = this.getPageLayoutRepository(transactionManager); + ): Promise> { + 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 { - const pageLayout = await this.pageLayoutRepository.findOne({ - select: { - id: true, - deletedAt: true, - }, - where: { - id, - workspaceId, - }, - withDeleted: true, - }); + async restore( + id: string, + workspaceId: string, + ): Promise> { + 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, + }), + ); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-tab-to-page-layout-tab-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-tab-to-page-layout-tab-dto.util.ts new file mode 100644 index 00000000000..9dccf9ea309 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-tab-to-page-layout-tab-dto.util.ts @@ -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 => { + const { + createdAt, + updatedAt, + deletedAt, + widgetIds: _widgetIds, + ...rest + } = flatPageLayoutTab; + + return { + ...rest, + createdAt: new Date(createdAt), + updatedAt: new Date(updatedAt), + deletedAt: deletedAt ? new Date(deletedAt) : null, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-tab-with-widgets-to-page-layout-tab-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-tab-with-widgets-to-page-layout-tab-dto.util.ts new file mode 100644 index 00000000000..7a53673bd00 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-tab-with-widgets-to-page-layout-tab-dto.util.ts @@ -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), + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-to-page-layout-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-to-page-layout-dto.util.ts new file mode 100644 index 00000000000..3c7670c983b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-to-page-layout-dto.util.ts @@ -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 => { + const { + createdAt, + updatedAt, + deletedAt, + tabIds: _tabIds, + ...rest + } = flatPageLayout; + + return { + ...rest, + createdAt: new Date(createdAt), + updatedAt: new Date(updatedAt), + deletedAt: deletedAt ? new Date(deletedAt) : null, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util.ts new file mode 100644 index 00000000000..92dc032d5de --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util.ts @@ -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, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.util.ts new file mode 100644 index 00000000000..6ff7a8acf6a --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.util.ts @@ -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), + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception-handler.util.ts index 255be726070..d46fdb44b0d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception-handler.util.ts @@ -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: diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter.ts index 66a918c2451..0a3738559cd 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter.ts @@ -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); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-page-layout-tab-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-page-layout-tab-validator.service.ts index 24f76c4f614..c1fc8e94580 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-page-layout-tab-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-page-layout-tab-validator.service.ts @@ -4,11 +4,12 @@ import { msg, t } from '@lingui/core/macro'; import { type ALL_METADATA_NAME } from 'twenty-shared/metadata'; 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 { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; +import { PageLayoutExceptionCode } from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception'; import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type'; -import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type'; +import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type'; const PAGE_LAYOUT_TAB_EXCEPTION_CODE = { PAGE_LAYOUT_TAB_NOT_FOUND: 'PAGE_LAYOUT_TAB_NOT_FOUND', @@ -18,6 +19,7 @@ const PAGE_LAYOUT_TAB_EXCEPTION_CODE = { export class FlatPageLayoutTabValidatorService { public validateFlatPageLayoutTabCreation({ flatEntityToValidate: flatPageLayoutTab, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { flatPageLayoutMaps }, }: FlatEntityValidationArgs< typeof ALL_METADATA_NAME.pageLayoutTab >): FailedFlatEntityValidation { @@ -30,6 +32,19 @@ export class FlatPageLayoutTabValidatorService { }, }; + const referencedPageLayout = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: flatPageLayoutTab.pageLayoutId, + flatEntityMaps: flatPageLayoutMaps, + }); + + if (!isDefined(referencedPageLayout)) { + validationResult.errors.push({ + code: PageLayoutExceptionCode.PAGE_LAYOUT_NOT_FOUND, + message: t`Page layout not found`, + userFriendlyMessage: msg`Page layout not found`, + }); + } + return validationResult; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-page-layout-widget-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-page-layout-widget-validator.service.ts index 086d49bbf31..b63885e1f4e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-page-layout-widget-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-page-layout-widget-validator.service.ts @@ -4,7 +4,9 @@ import { msg, t } from '@lingui/core/macro'; import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; import { isDefined } from 'twenty-shared/utils'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; +import { PageLayoutTabExceptionCode } from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-tab.exception'; import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-widget.exception'; import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type'; @@ -95,6 +97,7 @@ export class FlatPageLayoutWidgetValidatorService { public validateFlatPageLayoutWidgetCreation({ flatEntityToValidate: flatPageLayoutWidgetToValidate, optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatPageLayoutTabMaps, flatPageLayoutWidgetMaps: optimisticFlatPageLayoutWidgetMaps, }, }: FlatEntityValidationArgs< @@ -124,6 +127,19 @@ export class FlatPageLayoutWidgetValidatorService { }); } + const referencedPageLayoutTab = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: flatPageLayoutWidgetToValidate.pageLayoutTabId, + flatEntityMaps: flatPageLayoutTabMaps, + }); + + if (!isDefined(referencedPageLayoutTab)) { + validationResult.errors.push({ + code: PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND, + message: t`Page layout tab not found`, + userFriendlyMessage: msg`Page layout tab not found`, + }); + } + return validationResult; } } diff --git a/packages/twenty-server/src/modules/dashboard/query-hooks/dashboard-create-one.pre-query.hook.ts b/packages/twenty-server/src/modules/dashboard/query-hooks/dashboard-create-one.pre-query.hook.ts index d06eb6efbc8..47c492f47fe 100644 --- a/packages/twenty-server/src/modules/dashboard/query-hooks/dashboard-create-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/dashboard/query-hooks/dashboard-create-one.pre-query.hook.ts @@ -1,18 +1,16 @@ import { Injectable } from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; -import { DataSource } from 'typeorm'; import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { type CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; 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'; -import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { type DashboardWorkspaceEntity } from 'src/modules/dashboard/standard-objects/dashboard.workspace-entity'; @Injectable() @@ -23,8 +21,6 @@ export class DashboardCreateOnePreQueryHook constructor( private readonly pageLayoutService: PageLayoutService, private readonly pageLayoutTabService: PageLayoutTabService, - @InjectDataSource() - private readonly coreDataSource: DataSource, ) {} async execute( @@ -36,29 +32,25 @@ export class DashboardCreateOnePreQueryHook assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); - return await this.coreDataSource.transaction(async (manager) => { - const pageLayout = await this.pageLayoutService.create( - { - type: PageLayoutType.DASHBOARD, - objectMetadataId: null, - name: 'Dashboard Layout', - }, - workspace.id, - manager, - ); + const pageLayout = await this.pageLayoutService.create( + { + type: PageLayoutType.DASHBOARD, + objectMetadataId: null, + name: 'Dashboard Layout', + }, + workspace.id, + ); - await this.pageLayoutTabService.create( - { - title: 'Tab 1', - pageLayoutId: pageLayout.id, - }, - workspace.id, - manager, - ); + await this.pageLayoutTabService.create( + { + title: 'Tab 1', + pageLayoutId: pageLayout.id, + }, + workspace.id, + ); - payload.data.pageLayoutId = pageLayout.id; + payload.data.pageLayoutId = pageLayout.id; - return payload; - }); + return payload; } } diff --git a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-resolver.integration-spec.ts deleted file mode 100644 index 680c9e02de1..00000000000 --- a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-resolver.integration-spec.ts +++ /dev/null @@ -1,542 +0,0 @@ -import gql from 'graphql-tag'; -import { TEST_NOT_EXISTING_PAGE_LAYOUT_ID } from 'test/integration/constants/test-page-layout-ids.constants'; -import { createPageLayoutOperationFactory } from 'test/integration/graphql/utils/create-page-layout-operation-factory.util'; -import { deletePageLayoutOperationFactory } from 'test/integration/graphql/utils/delete-page-layout-operation-factory.util'; -import { destroyPageLayoutOperationFactory } from 'test/integration/graphql/utils/destroy-page-layout-operation-factory.util'; -import { findPageLayoutOperationFactory } from 'test/integration/graphql/utils/find-page-layout-operation-factory.util'; -import { findPageLayoutsOperationFactory } from 'test/integration/graphql/utils/find-page-layouts-operation-factory.util'; -import { - assertGraphQLErrorResponse, - assertGraphQLSuccessfulResponse, -} from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; -import { createTestPageLayoutWithGraphQL } from 'test/integration/graphql/utils/page-layout-graphql.util'; -import { restorePageLayoutOperationFactory } from 'test/integration/graphql/utils/restore-page-layout-operation-factory.util'; -import { updatePageLayoutOperationFactory } from 'test/integration/graphql/utils/update-page-layout-operation-factory.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { - assertPageLayoutStructure, - cleanupPageLayoutRecords, -} from 'test/integration/utils/page-layout-test.util'; - -import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; -import { - PageLayoutExceptionMessageKey, - generatePageLayoutExceptionMessage, -} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception'; - -describe('Page Layout Resolver', () => { - let testObjectMetadataId: string; - - beforeAll(async () => { - const { - data: { - createOneObject: { id: objectMetadataId }, - }, - } = await createOneObjectMetadata({ - input: { - nameSingular: 'myTestPageLayoutObject', - namePlural: 'myTestPageLayoutObjects', - labelSingular: 'My Test Page Layout Object', - labelPlural: 'My Test Page Layout Objects', - icon: 'IconLayout', - }, - }); - - testObjectMetadataId = objectMetadataId; - }); - - afterAll(async () => { - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: testObjectMetadataId, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - input: { idToDelete: testObjectMetadataId }, - }); - await cleanupPageLayoutRecords(); - }); - - beforeEach(async () => { - await cleanupPageLayoutRecords(); - }); - - describe('getPageLayouts', () => { - it('should return all page layouts for workspace when no objectMetadataId provided', async () => { - const pageLayoutName = 'Test Page Layout for Workspace'; - - await createTestPageLayoutWithGraphQL({ - name: pageLayoutName, - objectMetadataId: testObjectMetadataId, - }); - - const operation = findPageLayoutsOperationFactory(); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - expect(response.body.data.getPageLayouts).toHaveLength(1); - assertPageLayoutStructure(response.body.data.getPageLayouts[0], { - name: pageLayoutName, - objectMetadataId: testObjectMetadataId, - type: PageLayoutType.RECORD_PAGE, - }); - }); - - it('should filter page layouts by objectMetadataId when provided', async () => { - const object1PageLayoutName = 'Page Layout for Object 1'; - const object2PageLayoutName = 'Page Layout for Object 2'; - - const { - data: { - createOneObject: { id: objectMetadata2Id }, - }, - } = await createOneObjectMetadata({ - input: { - nameSingular: 'myTestPageLayoutObject2', - namePlural: 'myTestPageLayoutObjects2', - labelSingular: 'My Test Page Layout Object 2', - labelPlural: 'My Test Page Layout Objects 2', - icon: 'IconLayout2', - isLabelSyncedWithName: false, - }, - }); - - await Promise.all([ - createTestPageLayoutWithGraphQL({ - name: object1PageLayoutName, - objectMetadataId: testObjectMetadataId, - }), - createTestPageLayoutWithGraphQL({ - name: object2PageLayoutName, - objectMetadataId: objectMetadata2Id, - }), - ]); - - const operation = findPageLayoutsOperationFactory({ - objectMetadataId: testObjectMetadataId, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - expect(response.body.data.getPageLayouts).toHaveLength(1); - assertPageLayoutStructure(response.body.data.getPageLayouts[0], { - name: object1PageLayoutName, - objectMetadataId: testObjectMetadataId, - }); - - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: objectMetadata2Id, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - input: { idToDelete: objectMetadata2Id }, - }); - }); - }); - - describe('getPageLayout', () => { - it('should throw when page layout does not exist', async () => { - const operation = findPageLayoutOperationFactory({ - pageLayoutId: TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutExceptionMessage( - PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - ), - ); - }); - - it('should return page layout when it exists', async () => { - const pageLayoutName = 'Test Page Layout for Get'; - - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: pageLayoutName, - objectMetadataId: testObjectMetadataId, - }); - - const operation = findPageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - assertPageLayoutStructure(response.body.data.getPageLayout, { - id: pageLayout.id, - name: pageLayoutName, - objectMetadataId: testObjectMetadataId, - type: PageLayoutType.RECORD_PAGE, - }); - }); - }); - - describe('createPageLayout', () => { - it('should create a new page layout with all properties', async () => { - const input = { - name: 'Dashboard Page Layout', - type: PageLayoutType.DASHBOARD, - objectMetadataId: testObjectMetadataId, - }; - - const operation = createPageLayoutOperationFactory({ data: input }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - - const createdPageLayout = response.body.data.createPageLayout; - - assertPageLayoutStructure(createdPageLayout, { - name: input.name, - type: input.type, - objectMetadataId: input.objectMetadataId, - deletedAt: null, - }); - }); - - it('should create a page layout with minimum required fields', async () => { - const input = { - name: 'Minimal Page Layout', - }; - - const operation = createPageLayoutOperationFactory({ data: input }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - - const createdPageLayout = response.body.data.createPageLayout; - - assertPageLayoutStructure(createdPageLayout, { - name: input.name, - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: null, - deletedAt: null, - }); - }); - }); - - describe('updatePageLayout', () => { - it('should update an existing page layout', async () => { - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Original Page Layout', - type: PageLayoutType.RECORD_PAGE, - }); - - const updateInput = { - name: 'Updated Page Layout', - type: PageLayoutType.DASHBOARD, - }; - - const operation = updatePageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - data: updateInput, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - assertPageLayoutStructure(response.body.data.updatePageLayout, { - id: pageLayout.id, - name: updateInput.name, - type: updateInput.type, - deletedAt: null, - }); - }); - - it('should update only provided fields', async () => { - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Original Page Layout', - type: PageLayoutType.RECORD_PAGE, - }); - - const updateInput = { - name: 'Updated Name Only', - }; - - const operation = updatePageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - data: updateInput, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - assertPageLayoutStructure(response.body.data.updatePageLayout, { - id: pageLayout.id, - name: updateInput.name, - type: PageLayoutType.RECORD_PAGE, - deletedAt: null, - }); - }); - - it('should throw error when updating non-existent page layout', async () => { - const operation = updatePageLayoutOperationFactory({ - pageLayoutId: TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - data: { name: 'Non-existent Page Layout' }, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutExceptionMessage( - PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - ), - ); - }); - }); - - describe('deletePageLayout', () => { - it('should delete an existing page layout (soft delete)', async () => { - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Page Layout to Delete', - }); - - const deleteOperation = deletePageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - }); - const deleteResponse = await makeGraphqlAPIRequest(deleteOperation); - - assertGraphQLSuccessfulResponse(deleteResponse); - assertPageLayoutStructure( - deleteResponse.body.data.deletePageLayout, - pageLayout, - ); - - const getOperation = findPageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - }); - const getResponse = await makeGraphqlAPIRequest(getOperation); - - assertGraphQLErrorResponse( - getResponse, - ErrorCode.NOT_FOUND, - generatePageLayoutExceptionMessage( - PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND, - pageLayout.id, - ), - ); - }); - - it('should throw an error when deleting non-existent page layout', async () => { - const operation = deletePageLayoutOperationFactory({ - pageLayoutId: TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutExceptionMessage( - PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - ), - ); - }); - }); - - describe('destroyPageLayout', () => { - it('should destroy an existing page layout (hard delete)', async () => { - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Page Layout to Destroy', - }); - - const destroyOperation = destroyPageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - }); - const destroyResponse = await makeGraphqlAPIRequest(destroyOperation); - - assertGraphQLSuccessfulResponse(destroyResponse); - expect(destroyResponse.body.data.destroyPageLayout).toBe(true); - }); - - it('should throw an error when destroying non-existent page layout', async () => { - const operation = destroyPageLayoutOperationFactory({ - pageLayoutId: TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutExceptionMessage( - PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - ), - ); - }); - - it('should destroy all associated dashboards when page layout is of type dashboard', async () => { - const dashboardId = '20202020-304c-44f2-ba7b-070762ff0e8a'; - - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Page Layout to Destroy', - type: PageLayoutType.DASHBOARD, - }); - - const findOneDashboardOperation = { - query: gql` - query Dashboard($filter: DashboardFilterInput!) { - dashboard(filter: $filter) { - id - } - } - `, - variables: { - filter: { id: { eq: dashboardId } }, - }, - }; - - await makeGraphqlAPIRequest({ - query: gql` - mutation CreateDashboard($input: CreateDashboardInput!) { - createDashboard(input: $input) { - pageLayoutId - id - title - } - } - `, - variables: { - input: { - id: dashboardId, - name: 'Dashboard to Destroy', - pageLayoutId: pageLayout.id, - }, - }, - }); - - const findOneDashboardResponseBeforeDestroy = await makeGraphqlAPIRequest( - findOneDashboardOperation, - ); - - expect( - findOneDashboardResponseBeforeDestroy.body.data.dashboard, - ).toBeDefined(); - - const destroyOperation = destroyPageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - }); - const destroyResponse = await makeGraphqlAPIRequest(destroyOperation); - - const findOneDashboardResponseAfterDestroy = await makeGraphqlAPIRequest( - findOneDashboardOperation, - ); - - assertGraphQLSuccessfulResponse(destroyResponse); - expect(destroyResponse.body.data.destroyPageLayout).toBe(true); - - assertGraphQLErrorResponse( - findOneDashboardResponseAfterDestroy, - ErrorCode.NOT_FOUND, - ); - }); - }); - - describe('restorePageLayout', () => { - it('should restore a soft deleted page layout', async () => { - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Page Layout to Restore', - type: PageLayoutType.RECORD_INDEX, - }); - - const deleteOperation = deletePageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - }); - const deleteResponse = await makeGraphqlAPIRequest(deleteOperation); - - assertGraphQLSuccessfulResponse(deleteResponse); - - const restoreOperation = restorePageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - }); - const restoreResponse = await makeGraphqlAPIRequest(restoreOperation); - - assertGraphQLSuccessfulResponse(restoreResponse); - assertPageLayoutStructure(restoreResponse.body.data.restorePageLayout, { - id: pageLayout.id, - name: 'Page Layout to Restore', - type: PageLayoutType.RECORD_INDEX, - deletedAt: null, - }); - - const getOperation = findPageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - }); - const getResponse = await makeGraphqlAPIRequest(getOperation); - - assertGraphQLSuccessfulResponse(getResponse); - assertPageLayoutStructure(getResponse.body.data.getPageLayout, { - id: pageLayout.id, - name: 'Page Layout to Restore', - type: PageLayoutType.RECORD_INDEX, - deletedAt: null, - }); - }); - - it('should throw an error when restoring non-existent page layout', async () => { - const operation = restorePageLayoutOperationFactory({ - pageLayoutId: TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutExceptionMessage( - PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - ), - ); - }); - }); - - describe('tabs resolver field', () => { - it('should resolve tabs field for page layout', async () => { - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Page Layout with Tabs', - }); - - const operation = findPageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - gqlFields: ` - id - name - type - createdAt - updatedAt - deletedAt - tabs { - id - title - position - pageLayoutId - } - `, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - assertPageLayoutStructure(response.body.data.getPageLayout, { - id: pageLayout.id, - name: 'Page Layout with Tabs', - tabs: expect.any(Array), - }); - }); - }); -}); diff --git a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-tab-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-tab-resolver.integration-spec.ts deleted file mode 100644 index efe3f65599a..00000000000 --- a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-tab-resolver.integration-spec.ts +++ /dev/null @@ -1,458 +0,0 @@ -import { TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID } from 'test/integration/constants/test-page-layout-tab-ids.constants'; -import { createPageLayoutTabOperationFactory } from 'test/integration/graphql/utils/create-page-layout-tab-operation-factory.util'; -import { deletePageLayoutOperationFactory } from 'test/integration/graphql/utils/delete-page-layout-operation-factory.util'; -import { deletePageLayoutTabOperationFactory } from 'test/integration/graphql/utils/delete-page-layout-tab-operation-factory.util'; -import { destroyPageLayoutTabOperationFactory } from 'test/integration/graphql/utils/destroy-page-layout-tab-operation-factory.util'; -import { findPageLayoutTabOperationFactory } from 'test/integration/graphql/utils/find-page-layout-tab-operation-factory.util'; -import { findPageLayoutTabsOperationFactory } from 'test/integration/graphql/utils/find-page-layout-tabs-operation-factory.util'; -import { - assertGraphQLErrorResponse, - assertGraphQLSuccessfulResponse, -} from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; -import { - cleanupPageLayoutRecordsWithGraphQL, - createTestPageLayoutWithGraphQL, -} from 'test/integration/graphql/utils/page-layout-graphql.util'; -import { - cleanupPageLayoutTabRecordsWithGraphQL, - createTestPageLayoutTabWithGraphQL, -} from 'test/integration/graphql/utils/page-layout-tab-graphql.util'; -import { restorePageLayoutTabOperationFactory } from 'test/integration/graphql/utils/restore-page-layout-tab-operation-factory.util'; -import { updatePageLayoutTabOperationFactory } from 'test/integration/graphql/utils/update-page-layout-tab-operation-factory.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { assertPageLayoutTabStructure } from 'test/integration/utils/page-layout-tab-test.util'; - -import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; -import { - PageLayoutTabExceptionMessageKey, - generatePageLayoutTabExceptionMessage, -} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-tab.exception'; - -describe('Page Layout Tab Resolver', () => { - let testObjectMetadataId: string; - let testPageLayoutId: string; - - beforeAll(async () => { - const { - data: { - createOneObject: { id: objectMetadataId }, - }, - } = await createOneObjectMetadata({ - input: { - nameSingular: 'myTestPageLayoutTabObject', - namePlural: 'myTestPageLayoutTabObjects', - labelSingular: 'My Test Page Layout Tab Object', - labelPlural: 'My Test Page Layout Tab Objects', - icon: 'IconTab', - }, - }); - - testObjectMetadataId = objectMetadataId; - - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Test Page Layout for Tabs', - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - testPageLayoutId = pageLayout.id; - }); - - afterAll(async () => { - await cleanupPageLayoutRecordsWithGraphQL(); - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: testObjectMetadataId, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - input: { idToDelete: testObjectMetadataId }, - }); - }); - - afterEach(async () => { - await cleanupPageLayoutTabRecordsWithGraphQL(testPageLayoutId); - }); - - describe('getPageLayoutTabs', () => { - it('should return empty array when no page layout tabs exist', async () => { - const operation = findPageLayoutTabsOperationFactory({ - pageLayoutId: testPageLayoutId, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - expect(response.body.data.getPageLayoutTabs).toEqual([]); - }); - - it('should return all page layout tabs for a specific page layout', async () => { - const input1 = { - title: 'Tab 1', - position: 0, - pageLayoutId: testPageLayoutId, - }; - const input2 = { - title: 'Tab 2', - position: 1, - pageLayoutId: testPageLayoutId, - }; - - await Promise.all([ - createTestPageLayoutTabWithGraphQL(input1), - createTestPageLayoutTabWithGraphQL(input2), - ]); - - const operation = findPageLayoutTabsOperationFactory({ - pageLayoutId: testPageLayoutId, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - expect(response.body.data.getPageLayoutTabs).toHaveLength(2); - - const tabs = response.body.data.getPageLayoutTabs.sort( - (a: { position: number }, b: { position: number }) => - a.position - b.position, - ); - - assertPageLayoutTabStructure(tabs[0], input1); - assertPageLayoutTabStructure(tabs[1], input2); - }); - }); - - describe('getPageLayoutTab', () => { - it('should throw when page layout tab does not exist', async () => { - const operation = findPageLayoutTabOperationFactory({ - pageLayoutTabId: TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - ), - ); - }); - - it('should return page layout tab when it exists', async () => { - const tabTitle = 'Tab'; - - const input = { - title: tabTitle, - position: 2, - pageLayoutId: testPageLayoutId, - }; - - const tab = await createTestPageLayoutTabWithGraphQL(input); - - const operation = findPageLayoutTabOperationFactory({ - pageLayoutTabId: tab.id, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - assertPageLayoutTabStructure(response.body.data.getPageLayoutTab, input); - }); - }); - - describe('createPageLayoutTab', () => { - it('should create a new page layout tab with all properties', async () => { - const input = { - title: 'New Tab', - position: 5, - pageLayoutId: testPageLayoutId, - }; - - const operation = createPageLayoutTabOperationFactory({ data: input }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - - const createdTab = response.body.data.createPageLayoutTab; - - assertPageLayoutTabStructure(createdTab, input); - }); - - it('should create a page layout tab with minimum required fields', async () => { - const input = { - title: 'Minimal Tab', - pageLayoutId: testPageLayoutId, - }; - - const operation = createPageLayoutTabOperationFactory({ data: input }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - - const createdTab = response.body.data.createPageLayoutTab; - - assertPageLayoutTabStructure(createdTab, { - title: input.title, - position: 0, - pageLayoutId: input.pageLayoutId, - deletedAt: null, - }); - }); - }); - - describe('updatePageLayoutTab', () => { - it('should update an existing page layout tab', async () => { - const input = { - title: 'Original Tab', - position: 1, - pageLayoutId: testPageLayoutId, - }; - - const tab = await createTestPageLayoutTabWithGraphQL(input); - - const updateInput = { - title: 'Updated Tab', - position: 3, - }; - - const operation = updatePageLayoutTabOperationFactory({ - pageLayoutTabId: tab.id, - data: updateInput, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - assertPageLayoutTabStructure( - response.body.data.updatePageLayoutTab, - updateInput, - ); - }); - - it('should update only provided fields', async () => { - const input = { - title: 'Original Tab', - position: 1, - pageLayoutId: testPageLayoutId, - }; - - const tab = await createTestPageLayoutTabWithGraphQL(input); - - const updateInput = { - title: 'Updated Title Only', - }; - - const operation = updatePageLayoutTabOperationFactory({ - pageLayoutTabId: tab.id, - data: updateInput, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - assertPageLayoutTabStructure(response.body.data.updatePageLayoutTab, { - ...input, - ...updateInput, - }); - }); - - it('should throw error when updating non-existent page layout tab', async () => { - const operation = updatePageLayoutTabOperationFactory({ - pageLayoutTabId: TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - data: { title: 'Non-existent Tab' }, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - ), - ); - }); - }); - - describe('deletePageLayoutTab', () => { - it('should delete an existing page layout tab (soft delete)', async () => { - const tab = await createTestPageLayoutTabWithGraphQL({ - title: 'Tab to Delete', - pageLayoutId: testPageLayoutId, - }); - - const deleteOperation = deletePageLayoutTabOperationFactory({ - pageLayoutTabId: tab.id, - }); - const deleteResponse = await makeGraphqlAPIRequest(deleteOperation); - - assertGraphQLSuccessfulResponse(deleteResponse); - expect(deleteResponse.body.data.deletePageLayoutTab).toBe(true); - - const getOperation = findPageLayoutTabOperationFactory({ - pageLayoutTabId: tab.id, - }); - const getResponse = await makeGraphqlAPIRequest(getOperation); - - assertGraphQLErrorResponse( - getResponse, - ErrorCode.NOT_FOUND, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, - tab.id, - ), - ); - }); - - it('should throw an error when deleting non-existent page layout tab', async () => { - const operation = deletePageLayoutTabOperationFactory({ - pageLayoutTabId: TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - ), - ); - }); - }); - - describe('destroyPageLayoutTab', () => { - it('should destroy an existing page layout tab (hard delete)', async () => { - const tab = await createTestPageLayoutTabWithGraphQL({ - title: 'Tab to Destroy', - pageLayoutId: testPageLayoutId, - }); - - const destroyOperation = destroyPageLayoutTabOperationFactory({ - pageLayoutTabId: tab.id, - }); - const destroyResponse = await makeGraphqlAPIRequest(destroyOperation); - - assertGraphQLSuccessfulResponse(destroyResponse); - expect(destroyResponse.body.data.destroyPageLayoutTab).toBe(true); - }); - - it('should throw an error when destroying non-existent page layout tab', async () => { - const operation = destroyPageLayoutTabOperationFactory({ - pageLayoutTabId: TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - ), - ); - }); - }); - - describe('restorePageLayoutTab', () => { - it('should restore a soft deleted page layout tab', async () => { - const input = { - title: 'Tab to Restore', - position: 2, - pageLayoutId: testPageLayoutId, - }; - - const tab = await createTestPageLayoutTabWithGraphQL(input); - - const deleteOperation = deletePageLayoutTabOperationFactory({ - pageLayoutTabId: tab.id, - }); - const deleteResponse = await makeGraphqlAPIRequest(deleteOperation); - - assertGraphQLSuccessfulResponse(deleteResponse); - - const restoreOperation = restorePageLayoutTabOperationFactory({ - pageLayoutTabId: tab.id, - }); - const restoreResponse = await makeGraphqlAPIRequest(restoreOperation); - - assertGraphQLSuccessfulResponse(restoreResponse); - assertPageLayoutTabStructure( - restoreResponse.body.data.restorePageLayoutTab, - input, - ); - - const getOperation = findPageLayoutTabOperationFactory({ - pageLayoutTabId: tab.id, - }); - const getResponse = await makeGraphqlAPIRequest(getOperation); - - assertGraphQLSuccessfulResponse(getResponse); - assertPageLayoutTabStructure( - getResponse.body.data.getPageLayoutTab, - input, - ); - }); - - it('should throw an error when restoring non-existent page layout tab', async () => { - const operation = restorePageLayoutTabOperationFactory({ - pageLayoutTabId: TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - ), - ); - }); - - it('should throw an error when restoring tab with deleted parent page layout', async () => { - const separatePageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Page Layout for Tab Restore Test', - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - const input = { - title: 'Tab with Deleted Page Layout', - pageLayoutId: separatePageLayout.id, - position: 0, - }; - - const tab = await createTestPageLayoutTabWithGraphQL(input); - - const deleteTabOperation = deletePageLayoutTabOperationFactory({ - pageLayoutTabId: tab.id, - }); - - await makeGraphqlAPIRequest(deleteTabOperation); - - const deletePageLayoutOperation = deletePageLayoutOperationFactory({ - pageLayoutId: separatePageLayout.id, - }); - - await makeGraphqlAPIRequest(deletePageLayoutOperation); - - const restoreOperation = restorePageLayoutTabOperationFactory({ - pageLayoutTabId: tab.id, - }); - const restoreResponse = await makeGraphqlAPIRequest(restoreOperation); - - assertGraphQLErrorResponse( - restoreResponse, - ErrorCode.BAD_USER_INPUT, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND, - ), - ); - }); - }); -}); diff --git a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-update-with-tabs-and-widgets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-update-with-tabs-and-widgets.integration-spec.ts deleted file mode 100644 index dfa6caf7a69..00000000000 --- a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-update-with-tabs-and-widgets.integration-spec.ts +++ /dev/null @@ -1,576 +0,0 @@ -import { - TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL, - TEST_IFRAME_CONFIG, - TEST_IFRAME_CONFIG_ALTERNATIVE, - TEST_NUMBER_CHART_CONFIG_MINIMAL, -} from 'test/integration/constants/widget-configuration-test-data.constants'; -import { findPageLayoutOperationFactory } from 'test/integration/graphql/utils/find-page-layout-operation-factory.util'; -import { - assertGraphQLErrorResponse, - assertGraphQLSuccessfulResponse, -} from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; -import { createTestPageLayoutWithGraphQL } from 'test/integration/graphql/utils/page-layout-graphql.util'; -import { updatePageLayoutWithTabsOperationFactory } from 'test/integration/graphql/utils/update-page-layout-with-tabs-operation-factory.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { cleanupPageLayoutTabRecords } from 'test/integration/utils/page-layout-tab-test.util'; -import { - assertPageLayoutStructure, - cleanupPageLayoutRecords, -} from 'test/integration/utils/page-layout-test.util'; -import { cleanupPageLayoutWidgetRecords } from 'test/integration/utils/page-layout-widget-test.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; - -import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -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'; - -const existingTabId = '20202020-e02c-4292-9994-42695c4e41e8'; -const tabToUpdateId = '20202020-974a-4480-b814-1fca24937132'; -const tabToDeleteId = '20202020-a510-49e6-a9d9-0ff3e5c6a1ae'; -const newTabId = '20202020-1db1-4c18-8804-6c700a8106a6'; - -const existingWidgetId = '20202020-afbe-46a3-a759-9a10ea7f5888'; -const widgetToUpdateId = '20202020-c312-4342-93c0-f02c92c4a609'; -const widgetToDeleteId = '20202020-8a0b-4ec2-8784-af4adb27c18a'; -const newWidgetId = '20202020-da21-4321-b313-699eeff00798'; -const anotherNewWidgetId = '20202020-3937-4617-bc88-8a811f769c90'; - -describe('Page Layout Update With Tabs And Widgets Integration', () => { - let testObjectMetadataId: string; - - beforeAll(async () => { - const { - data: { - createOneObject: { id: objectMetadataId }, - }, - } = await createOneObjectMetadata({ - input: { - nameSingular: 'myTestUpdatePageLayoutObject', - namePlural: 'myTestUpdatePageLayoutObjects', - labelSingular: 'My Test Update Page Layout Object', - labelPlural: 'My Test Update Page Layout Objects', - icon: 'IconUpdate', - }, - }); - - testObjectMetadataId = objectMetadataId; - }); - - afterAll(async () => { - await updateOneObjectMetadata({ - input: { - idToUpdate: testObjectMetadataId, - updatePayload: { - isActive: false, - }, - }, - }); - - await deleteOneObjectMetadata({ - input: { idToDelete: testObjectMetadataId }, - }); - }); - - afterEach(async () => { - await cleanupPageLayoutRecords(); - await cleanupPageLayoutTabRecords(); - await cleanupPageLayoutWidgetRecords(); - }); - - describe('updatePageLayoutWithTabsAndWidgets', () => { - it('should handle complex page layout update with tab and widget CRUD operations', async () => { - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Initial Page Layout', - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - const initialUpdateInput = { - name: 'Updated Page Layout', - type: PageLayoutType.DASHBOARD, - objectMetadataId: testObjectMetadataId, - tabs: [ - { - id: existingTabId, - title: 'Existing Tab', - position: 1, - widgets: [ - { - id: existingWidgetId, - pageLayoutTabId: existingTabId, - title: 'Existing Widget', - type: WidgetType.VIEW, - objectMetadataId: testObjectMetadataId, - gridPosition: { - row: 0, - column: 0, - rowSpan: 2, - columnSpan: 2, - }, - configuration: null, - }, - { - id: widgetToUpdateId, - pageLayoutTabId: existingTabId, - title: 'Widget To Update', - type: WidgetType.GRAPH, - objectMetadataId: testObjectMetadataId, - gridPosition: { - row: 0, - column: 2, - rowSpan: 1, - columnSpan: 1, - }, - configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL, - }, - { - id: widgetToDeleteId, - pageLayoutTabId: existingTabId, - title: 'Widget To Delete', - type: WidgetType.IFRAME, - objectMetadataId: null, - gridPosition: { - row: 1, - column: 2, - rowSpan: 1, - columnSpan: 1, - }, - configuration: TEST_IFRAME_CONFIG, - }, - ], - }, - { - id: tabToUpdateId, - title: 'Tab To Update', - position: 2, - widgets: [], - }, - { - id: tabToDeleteId, - title: 'Tab To Delete', - position: 3, - widgets: [], - }, - ], - }; - - const initialOperation = updatePageLayoutWithTabsOperationFactory({ - pageLayoutId: pageLayout.id, - data: initialUpdateInput, - }); - - const initialResponse = await makeGraphqlAPIRequest(initialOperation); - - assertGraphQLSuccessfulResponse(initialResponse); - - const initialResult = - initialResponse.body.data.updatePageLayoutWithTabsAndWidgets; - - assertPageLayoutStructure(initialResult, { - id: pageLayout.id, - name: 'Updated Page Layout', - type: PageLayoutType.DASHBOARD, - objectMetadataId: testObjectMetadataId, - }); - - expect(initialResult.tabs).toHaveLength(3); - expect(initialResult.tabs[0].widgets).toHaveLength(3); - - const complexUpdateInput = { - name: 'Final Page Layout', - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - tabs: [ - { - id: existingTabId, - title: 'Updated Existing Tab', - position: 1, - widgets: [ - { - id: existingWidgetId, - pageLayoutTabId: existingTabId, - title: 'Existing Widget', - type: WidgetType.VIEW, - objectMetadataId: testObjectMetadataId, - gridPosition: { - row: 0, - column: 0, - rowSpan: 2, - columnSpan: 2, - }, - configuration: null, - }, - { - id: widgetToUpdateId, - pageLayoutTabId: existingTabId, - title: 'Updated Widget Title', - type: WidgetType.GRAPH, - objectMetadataId: testObjectMetadataId, - gridPosition: { - row: 0, - column: 2, - rowSpan: 2, - columnSpan: 3, - }, - configuration: TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL, - }, - { - id: newWidgetId, - pageLayoutTabId: existingTabId, - title: 'New Widget', - type: WidgetType.FIELDS, - objectMetadataId: testObjectMetadataId, - gridPosition: { - row: 2, - column: 0, - rowSpan: 1, - columnSpan: 2, - }, - configuration: null, - }, - ], - }, - { - id: tabToUpdateId, - title: 'Completely Updated Tab', - position: 3, - widgets: [ - { - id: anotherNewWidgetId, - pageLayoutTabId: tabToUpdateId, - title: 'Another New Widget', - type: WidgetType.IFRAME, - objectMetadataId: null, - gridPosition: { - row: 0, - column: 0, - rowSpan: 1, - columnSpan: 1, - }, - configuration: TEST_IFRAME_CONFIG_ALTERNATIVE, - }, - ], - }, - { - id: newTabId, - title: 'Brand New Tab', - position: 2, - widgets: [], - }, - ], - }; - - const complexOperation = updatePageLayoutWithTabsOperationFactory({ - pageLayoutId: pageLayout.id, - data: complexUpdateInput, - }); - - const complexResponse = await makeGraphqlAPIRequest(complexOperation); - - assertGraphQLSuccessfulResponse(complexResponse); - - const finalResult: PageLayoutEntity = - complexResponse.body.data.updatePageLayoutWithTabsAndWidgets; - - assertPageLayoutStructure(finalResult, { - id: pageLayout.id, - name: 'Final Page Layout', - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - expect(finalResult.tabs).toHaveLength(3); - - const updatedExistingTab = finalResult.tabs.find( - (tab) => tab.id === existingTabId, - ); - const updatedTab = finalResult.tabs.find( - (tab) => tab.id === tabToUpdateId, - ); - const newTab = finalResult.tabs.find((tab) => tab.id === newTabId); - const deletedTab = finalResult.tabs.find( - (tab) => tab.id === tabToDeleteId, - ); - - expect(updatedExistingTab).toBeDefined(); - expect(updatedExistingTab?.title).toBe('Updated Existing Tab'); - expect(updatedExistingTab?.position).toBe(1); - - expect(updatedTab).toBeDefined(); - expect(updatedTab?.title).toBe('Completely Updated Tab'); - expect(updatedTab?.position).toBe(3); - - expect(newTab).toBeDefined(); - expect(newTab?.title).toBe('Brand New Tab'); - expect(newTab?.position).toBe(2); - - expect(deletedTab).toBeUndefined(); - - const firstTabWidgets = updatedExistingTab?.widgets; - - expect(firstTabWidgets).toHaveLength(3); - - const existingWidget = firstTabWidgets?.find( - (widget) => widget.id === existingWidgetId, - ); - const updatedWidget = firstTabWidgets?.find( - (widget) => widget.id === widgetToUpdateId, - ); - const newWidget = firstTabWidgets?.find( - (widget) => widget.id === newWidgetId, - ); - const deletedWidget = firstTabWidgets?.find( - (widget) => widget.id === widgetToDeleteId, - ); - - expect(existingWidget).toBeDefined(); - expect(existingWidget?.title).toBe('Existing Widget'); - expect(existingWidget?.type).toBe(WidgetType.VIEW); - - expect(updatedWidget).toBeDefined(); - expect(updatedWidget?.title).toBe('Updated Widget Title'); - expect(updatedWidget?.type).toBe(WidgetType.GRAPH); - expect(updatedWidget?.gridPosition.rowSpan).toBe(2); - expect(updatedWidget?.gridPosition.columnSpan).toBe(3); - - expect(newWidget).toBeDefined(); - expect(newWidget?.title).toBe('New Widget'); - expect(newWidget?.type).toBe(WidgetType.FIELDS); - - expect(deletedWidget).toBeUndefined(); - - const secondTabWidgets = updatedTab?.widgets; - - expect(secondTabWidgets).toHaveLength(1); - - const anotherNewWidget = secondTabWidgets?.find( - (widget) => widget.id === anotherNewWidgetId, - ); - - expect(anotherNewWidget).toBeDefined(); - expect(anotherNewWidget?.title).toBe('Another New Widget'); - expect(anotherNewWidget?.type).toBe(WidgetType.IFRAME); - - expect(newTab?.widgets).toHaveLength(0); - }); - - it('should handle transaction rollback on error during complex update', async () => { - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Test Transaction Rollback', - type: PageLayoutType.RECORD_PAGE, - }); - - const invalidUpdateInput = { - name: '', - type: PageLayoutType.DASHBOARD, - objectMetadataId: 'invalid-uuid', - tabs: [ - { - id: '20202020-9a4c-4f97-bd63-a5f5843ee610', - title: 'Test Tab', - position: 1, - widgets: [], - }, - ], - }; - - const operation = updatePageLayoutWithTabsOperationFactory({ - pageLayoutId: pageLayout.id, - data: invalidUpdateInput, - }); - - const response = await makeGraphqlAPIRequest(operation); - - expect(response.body.errors).toBeDefined(); - expect(response.body.errors.length).toBeGreaterThan(0); - - const finalPageLayout = findPageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - }); - const finalResponse = await makeGraphqlAPIRequest(finalPageLayout); - - assertGraphQLSuccessfulResponse(finalResponse); - const finalResult = finalResponse.body.data.getPageLayout; - - assertPageLayoutStructure(finalResult, { - id: pageLayout.id, - name: 'Test Transaction Rollback', - type: PageLayoutType.RECORD_PAGE, - }); - }); - - it('should throw when empty tabs array is provided', async () => { - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Test Empty Tabs', - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - const updateInput = { - name: 'Updated Layout With No Tabs', - type: PageLayoutType.DASHBOARD, - objectMetadataId: testObjectMetadataId, - tabs: [], - }; - - const operation = updatePageLayoutWithTabsOperationFactory({ - pageLayoutId: pageLayout.id, - data: updateInput, - }); - - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse(response, ErrorCode.BAD_USER_INPUT); - }); - - it('should reject invalid widget configurations', async () => { - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Test Invalid Config', - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - const invalidConfigInput = { - name: 'Layout with Invalid Widget Config', - type: PageLayoutType.DASHBOARD, - objectMetadataId: testObjectMetadataId, - tabs: [ - { - id: '20202020-1001-4001-a001-000000000001', - title: 'Tab with Invalid Widget', - position: 1, - widgets: [ - { - id: '20202020-1002-4002-a002-000000000002', - pageLayoutTabId: '20202020-1001-4001-a001-000000000001', - title: 'Invalid Iframe Widget', - type: WidgetType.IFRAME, - objectMetadataId: null, - gridPosition: { - row: 0, - column: 0, - rowSpan: 1, - columnSpan: 1, - }, - configuration: { url: 'not-a-valid-url' }, - }, - ], - }, - ], - }; - - const operation = updatePageLayoutWithTabsOperationFactory({ - pageLayoutId: pageLayout.id, - data: invalidConfigInput, - }); - - const response = await makeGraphqlAPIRequest(operation); - - expect(response.body.errors).toBeDefined(); - expect(response.body.errors[0].message).toContain( - 'Invalid configuration for widget "Invalid Iframe Widget" of type IFRAME', - ); - expect(response.body.errors[0].message).toContain('url must be'); - }); - - it('should accept valid widget configurations for each type', async () => { - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Test Valid Configs', - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - const validConfigInput = { - name: 'Layout with Valid Widget Configs', - type: PageLayoutType.DASHBOARD, - objectMetadataId: testObjectMetadataId, - tabs: [ - { - id: '20202020-1010-4010-a010-101010101010', - title: 'Tab with Valid Widgets', - position: 1, - widgets: [ - { - id: '20202020-1011-4011-a011-111111111111', - pageLayoutTabId: '20202020-1010-4010-a010-101010101010', - title: 'Valid Iframe Widget', - type: WidgetType.IFRAME, - objectMetadataId: null, - gridPosition: { - row: 0, - column: 0, - rowSpan: 1, - columnSpan: 1, - }, - configuration: TEST_IFRAME_CONFIG, - }, - { - id: '20202020-1012-4012-a012-121212121212', - pageLayoutTabId: '20202020-1010-4010-a010-101010101010', - title: 'Valid Graph Widget', - type: WidgetType.GRAPH, - objectMetadataId: testObjectMetadataId, - gridPosition: { - row: 1, - column: 0, - rowSpan: 2, - columnSpan: 2, - }, - configuration: TEST_NUMBER_CHART_CONFIG_MINIMAL, - }, - { - id: '20202020-1013-4013-a013-131313131313', - pageLayoutTabId: '20202020-1010-4010-a010-101010101010', - title: 'Valid View Widget', - type: WidgetType.VIEW, - objectMetadataId: testObjectMetadataId, - gridPosition: { - row: 0, - column: 1, - rowSpan: 1, - columnSpan: 1, - }, - configuration: null, - }, - ], - }, - ], - }; - - const operation = updatePageLayoutWithTabsOperationFactory({ - pageLayoutId: pageLayout.id, - data: validConfigInput, - }); - - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - - const result = response.body.data.updatePageLayoutWithTabsAndWidgets; - const widgets = result.tabs[0].widgets; - - expect(widgets).toHaveLength(3); - - const iframeWidget = widgets.find( - (w: any) => w.type === WidgetType.IFRAME, - ); - - expect(iframeWidget.configuration).toBeDefined(); - expect(iframeWidget.configuration.url).toBe(TEST_IFRAME_CONFIG.url); - - const graphWidget = widgets.find((w: any) => w.type === WidgetType.GRAPH); - - expect(graphWidget.configuration).toBeDefined(); - expect(graphWidget.configuration.graphType).toBe( - TEST_NUMBER_CHART_CONFIG_MINIMAL.graphType, - ); - - const viewWidget = widgets.find((w: any) => w.type === WidgetType.VIEW); - - expect(viewWidget.configuration).toBeNull(); - }); - }); -}); diff --git a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-widget-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-widget-resolver.integration-spec.ts deleted file mode 100644 index a095b8e68f6..00000000000 --- a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-widget-resolver.integration-spec.ts +++ /dev/null @@ -1,511 +0,0 @@ -import { TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID } from 'test/integration/constants/test-page-layout-widget-ids.constants'; -import { createPageLayoutWidgetOperationFactory } from 'test/integration/graphql/utils/create-page-layout-widget-operation-factory.util'; -import { deletePageLayoutTabOperationFactory } from 'test/integration/graphql/utils/delete-page-layout-tab-operation-factory.util'; -import { deletePageLayoutWidgetOperationFactory } from 'test/integration/graphql/utils/delete-page-layout-widget-operation-factory.util'; -import { destroyPageLayoutWidgetOperationFactory } from 'test/integration/graphql/utils/destroy-page-layout-widget-operation-factory.util'; -import { findPageLayoutWidgetOperationFactory } from 'test/integration/graphql/utils/find-page-layout-widget-operation-factory.util'; -import { findPageLayoutWidgetsOperationFactory } from 'test/integration/graphql/utils/find-page-layout-widgets-operation-factory.util'; -import { - assertGraphQLErrorResponse, - assertGraphQLSuccessfulResponse, -} from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; -import { - cleanupPageLayoutRecordsWithGraphQL, - createTestPageLayoutWithGraphQL, -} from 'test/integration/graphql/utils/page-layout-graphql.util'; -import { createTestPageLayoutTabWithGraphQL } from 'test/integration/graphql/utils/page-layout-tab-graphql.util'; -import { - cleanupPageLayoutWidgetRecordsWithGraphQL, - createTestPageLayoutWidgetWithGraphQL, -} from 'test/integration/graphql/utils/page-layout-widget-graphql.util'; -import { restorePageLayoutWidgetOperationFactory } from 'test/integration/graphql/utils/restore-page-layout-widget-operation-factory.util'; -import { updatePageLayoutWidgetOperationFactory } from 'test/integration/graphql/utils/update-page-layout-widget-operation-factory.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { assertPageLayoutWidgetStructure } from 'test/integration/utils/page-layout-widget-test.util'; -import { isDefined } from 'twenty-shared/utils'; - -import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { type PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-widget.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 { - PageLayoutWidgetExceptionMessageKey, - generatePageLayoutWidgetExceptionMessage, -} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-widget.exception'; - -describe('Page Layout Widget Resolver', () => { - let testObjectMetadataId: string; - let testPageLayoutId: string; - let testPageLayoutTabId: string; - - beforeAll(async () => { - const { - data: { - createOneObject: { id: objectMetadataId }, - }, - } = await createOneObjectMetadata({ - input: { - nameSingular: 'myTestPageLayoutWidgetObject', - namePlural: 'myTestPageLayoutWidgetObjects', - labelSingular: 'My Test Page Layout Widget Object', - labelPlural: 'My Test Page Layout Widget Objects', - icon: 'IconWidget', - }, - }); - - testObjectMetadataId = objectMetadataId; - - const pageLayout = await createTestPageLayoutWithGraphQL({ - name: 'Test Page Layout for Widgets', - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - testPageLayoutId = pageLayout.id; - - const pageLayoutTab = await createTestPageLayoutTabWithGraphQL({ - title: 'Test Tab for Widgets', - position: 0, - pageLayoutId: testPageLayoutId, - }); - - testPageLayoutTabId = pageLayoutTab.id; - }); - - afterAll(async () => { - await cleanupPageLayoutRecordsWithGraphQL(); - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: testObjectMetadataId, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - input: { idToDelete: testObjectMetadataId }, - }); - }); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - afterEach(async () => { - await cleanupPageLayoutWidgetRecordsWithGraphQL(testPageLayoutTabId); - }); - - describe('getPageLayoutWidgets', () => { - it('should return empty array when no page layout widgets exist', async () => { - const operation = findPageLayoutWidgetsOperationFactory({ - pageLayoutTabId: testPageLayoutTabId, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - expect(response.body.data.getPageLayoutWidgets).toEqual([]); - }); - - it('should return all page layout widgets for a specific page layout tab', async () => { - const widgetTitle1 = 'Widget 1'; - const widgetTitle2 = 'Widget 2'; - - const [{ id: widget1Id }, { id: widget2Id }] = await Promise.all([ - createTestPageLayoutWidgetWithGraphQL({ - title: widgetTitle1, - type: WidgetType.VIEW, - pageLayoutTabId: testPageLayoutTabId, - gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 3 }, - }), - createTestPageLayoutWidgetWithGraphQL({ - title: widgetTitle2, - type: WidgetType.GRAPH, - pageLayoutTabId: testPageLayoutTabId, - gridPosition: { row: 0, column: 3, rowSpan: 1, columnSpan: 2 }, - }), - ]); - - const operation = findPageLayoutWidgetsOperationFactory({ - pageLayoutTabId: testPageLayoutTabId, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - expect(response.body.data.getPageLayoutWidgets).toHaveLength(2); - - const widgets: PageLayoutWidgetEntity[] = - response.body.data.getPageLayoutWidgets ?? []; - - const widget1 = widgets.find((widget) => widget.id === widget1Id); - const widget2 = widgets.find((widget) => widget.id === widget2Id); - - if (!isDefined(widget1) || !isDefined(widget2)) { - throw new Error('Widget not found'); - } - - assertPageLayoutWidgetStructure(widget1, { - title: widgetTitle1, - type: WidgetType.VIEW, - pageLayoutTabId: testPageLayoutTabId, - }); - assertPageLayoutWidgetStructure(widget2, { - title: widgetTitle2, - type: WidgetType.GRAPH, - pageLayoutTabId: testPageLayoutTabId, - }); - }); - }); - - describe('getPageLayoutWidget', () => { - it('should throw when page layout widget does not exist', async () => { - const operation = findPageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - ), - ); - }); - - it('should return page layout widget when it exists', async () => { - const widgetTitle = 'Widget'; - - const input = { - title: widgetTitle, - type: WidgetType.FIELDS, - pageLayoutTabId: testPageLayoutTabId, - objectMetadataId: testObjectMetadataId, - gridPosition: { row: 1, column: 1, rowSpan: 1, columnSpan: 2 }, - configuration: null, - }; - - const widget = await createTestPageLayoutWidgetWithGraphQL(input); - - const operation = findPageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: widget.id, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - assertPageLayoutWidgetStructure( - response.body.data.getPageLayoutWidget, - input, - ); - }); - }); - - describe('createPageLayoutWidget', () => { - it('should create a new page layout widget with all properties', async () => { - const input = { - title: 'New Widget', - type: WidgetType.FIELDS, - pageLayoutTabId: testPageLayoutTabId, - objectMetadataId: testObjectMetadataId, - gridPosition: { row: 2, column: 1, rowSpan: 3, columnSpan: 4 }, - configuration: null, - }; - - const operation = createPageLayoutWidgetOperationFactory({ data: input }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - - const createdWidget = response.body.data.createPageLayoutWidget; - - assertPageLayoutWidgetStructure(createdWidget, input); - }); - - it('should create a page layout widget with minimum required fields', async () => { - const input = { - title: 'Widget', - type: WidgetType.VIEW, - pageLayoutTabId: testPageLayoutTabId, - gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, - }; - - const operation = createPageLayoutWidgetOperationFactory({ data: input }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - - const createdWidget = response.body.data.createPageLayoutWidget; - - assertPageLayoutWidgetStructure(createdWidget, input); - }); - }); - - describe('updatePageLayoutWidget', () => { - it('should update an existing page layout widget', async () => { - const widget = await createTestPageLayoutWidgetWithGraphQL({ - title: 'Widget', - type: WidgetType.VIEW, - pageLayoutTabId: testPageLayoutTabId, - gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, - }); - - const updateInput = { - title: 'Updated Widget', - type: WidgetType.IFRAME, - gridPosition: { row: 1, column: 2, rowSpan: 2, columnSpan: 3 }, - configuration: null, - }; - - const operation = updatePageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: widget.id, - data: updateInput, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - assertPageLayoutWidgetStructure( - response.body.data.updatePageLayoutWidget, - updateInput, - ); - }); - - it('should update only provided fields', async () => { - const widget = await createTestPageLayoutWidgetWithGraphQL({ - title: 'Widget', - type: WidgetType.VIEW, - pageLayoutTabId: testPageLayoutTabId, - gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, - }); - - const updateInput = { - title: 'Updated Widget', - }; - - const operation = updatePageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: widget.id, - data: updateInput, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - assertPageLayoutWidgetStructure( - response.body.data.updatePageLayoutWidget, - updateInput, - ); - }); - - it('should throw error when updating non-existent page layout widget', async () => { - const operation = updatePageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - data: { title: 'Non-existent Widget' }, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - ), - ); - }); - }); - - describe('deletePageLayoutWidget', () => { - it('should delete an existing page layout widget (soft delete)', async () => { - const input = { - title: 'Widget', - type: WidgetType.VIEW, - pageLayoutTabId: testPageLayoutTabId, - gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, - }; - - const widget = await createTestPageLayoutWidgetWithGraphQL(input); - - const deleteOperation = deletePageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: widget.id, - }); - const deleteResponse = await makeGraphqlAPIRequest(deleteOperation); - - assertGraphQLSuccessfulResponse(deleteResponse); - assertPageLayoutWidgetStructure( - deleteResponse.body.data.deletePageLayoutWidget, - input, - ); - - const getOperation = findPageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: widget.id, - }); - const getResponse = await makeGraphqlAPIRequest(getOperation); - - assertGraphQLErrorResponse( - getResponse, - ErrorCode.NOT_FOUND, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, - widget.id, - ), - ); - }); - - it('should throw an error when deleting non-existent page layout widget', async () => { - const operation = deletePageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - ), - ); - }); - }); - - describe('destroyPageLayoutWidget', () => { - it('should destroy an existing page layout widget (hard delete)', async () => { - const input = { - title: 'Widget', - type: WidgetType.VIEW, - pageLayoutTabId: testPageLayoutTabId, - gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, - }; - - const widget = await createTestPageLayoutWidgetWithGraphQL(input); - - const destroyOperation = destroyPageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: widget.id, - }); - const destroyResponse = await makeGraphqlAPIRequest(destroyOperation); - - assertGraphQLSuccessfulResponse(destroyResponse); - expect(destroyResponse.body.data.destroyPageLayoutWidget).toBe(true); - }); - - it('should throw an error when destroying non-existent page layout widget', async () => { - const operation = destroyPageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - ), - ); - }); - }); - - describe('restorePageLayoutWidget', () => { - it('should restore a soft deleted page layout widget', async () => { - const input = { - title: 'Widget', - type: WidgetType.GRAPH, - pageLayoutTabId: testPageLayoutTabId, - gridPosition: { row: 2, column: 1, rowSpan: 1, columnSpan: 2 }, - configuration: null, - }; - - const widget = await createTestPageLayoutWidgetWithGraphQL(input); - - const deleteOperation = deletePageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: widget.id, - }); - const deleteResponse = await makeGraphqlAPIRequest(deleteOperation); - - assertGraphQLSuccessfulResponse(deleteResponse); - - const restoreOperation = restorePageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: widget.id, - }); - const restoreResponse = await makeGraphqlAPIRequest(restoreOperation); - - assertGraphQLSuccessfulResponse(restoreResponse); - assertPageLayoutWidgetStructure( - restoreResponse.body.data.restorePageLayoutWidget, - input, - ); - - const getOperation = findPageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: widget.id, - }); - const getResponse = await makeGraphqlAPIRequest(getOperation); - - assertGraphQLSuccessfulResponse(getResponse); - assertPageLayoutWidgetStructure( - getResponse.body.data.getPageLayoutWidget, - input, - ); - }); - - it('should throw an error when restoring non-existent page layout widget', async () => { - const operation = restorePageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLErrorResponse( - response, - ErrorCode.NOT_FOUND, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - ), - ); - }); - - it('should throw an error when restoring widget with deleted parent tab', async () => { - const separateTab = await createTestPageLayoutTabWithGraphQL({ - title: 'Tab for Widget Restore Test', - pageLayoutId: testPageLayoutId, - position: 1, - }); - - const input = { - title: 'Widget with Deleted Tab', - type: WidgetType.GRAPH, - pageLayoutTabId: separateTab.id, - gridPosition: { row: 1, column: 1, rowSpan: 1, columnSpan: 1 }, - configuration: null, - }; - - const widget = await createTestPageLayoutWidgetWithGraphQL(input); - - const deleteWidgetOperation = deletePageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: widget.id, - }); - - await makeGraphqlAPIRequest(deleteWidgetOperation); - - const deleteTabOperation = deletePageLayoutTabOperationFactory({ - pageLayoutTabId: separateTab.id, - }); - - await makeGraphqlAPIRequest(deleteTabOperation); - - const restoreOperation = restorePageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: widget.id, - }); - const restoreResponse = await makeGraphqlAPIRequest(restoreOperation); - - assertGraphQLErrorResponse( - restoreResponse, - ErrorCode.BAD_USER_INPUT, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, - ), - ); - }); - }); -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/create-page-layout-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/create-page-layout-operation-factory.util.ts deleted file mode 100644 index b09f7c29e9a..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/create-page-layout-operation-factory.util.ts +++ /dev/null @@ -1,25 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -import { type CreatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout.input'; - -type CreatePageLayoutOperationFactoryParams = { - gqlFields?: string; - data: CreatePageLayoutInput; -}; - -export const createPageLayoutOperationFactory = ({ - gqlFields = PAGE_LAYOUT_GQL_FIELDS, - data, -}: CreatePageLayoutOperationFactoryParams) => ({ - query: gql` - mutation CreatePageLayout($input: CreatePageLayoutInput!) { - createPageLayout(input: $input) { - ${gqlFields} - } - } - `, - variables: { - input: data, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/create-page-layout-tab-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/create-page-layout-tab-operation-factory.util.ts deleted file mode 100644 index 104b2a98656..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/create-page-layout-tab-operation-factory.util.ts +++ /dev/null @@ -1,25 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_TAB_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -import { type CreatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout-tab.input'; - -type CreatePageLayoutTabOperationFactoryParams = { - gqlFields?: string; - data: CreatePageLayoutTabInput; -}; - -export const createPageLayoutTabOperationFactory = ({ - gqlFields = PAGE_LAYOUT_TAB_GQL_FIELDS, - data, -}: CreatePageLayoutTabOperationFactoryParams) => ({ - query: gql` - mutation CreatePageLayoutTab($input: CreatePageLayoutTabInput!) { - createPageLayoutTab(input: $input) { - ${gqlFields} - } - } - `, - variables: { - input: data, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/create-page-layout-widget-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/create-page-layout-widget-operation-factory.util.ts deleted file mode 100644 index d80a31cac0e..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/create-page-layout-widget-operation-factory.util.ts +++ /dev/null @@ -1,25 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_WIDGET_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -import { type CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout-widget.input'; - -type CreatePageLayoutWidgetOperationFactoryParams = { - gqlFields?: string; - data: CreatePageLayoutWidgetInput; -}; - -export const createPageLayoutWidgetOperationFactory = ({ - gqlFields = PAGE_LAYOUT_WIDGET_GQL_FIELDS, - data, -}: CreatePageLayoutWidgetOperationFactoryParams) => ({ - query: gql` - mutation CreatePageLayoutWidget($input: CreatePageLayoutWidgetInput!) { - createPageLayoutWidget(input: $input) { - ${gqlFields} - } - } - `, - variables: { - input: data, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/delete-page-layout-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/delete-page-layout-operation-factory.util.ts deleted file mode 100644 index 01a02e378a6..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/delete-page-layout-operation-factory.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -type DeletePageLayoutOperationFactoryParams = { - pageLayoutId: string; - gqlFields?: string; -}; - -export const deletePageLayoutOperationFactory = ({ - pageLayoutId, - gqlFields = PAGE_LAYOUT_GQL_FIELDS, -}: DeletePageLayoutOperationFactoryParams) => ({ - query: gql` - mutation DeletePageLayout($id: String!) { - deletePageLayout(id: $id) { - ${gqlFields} - } - } - `, - variables: { - id: pageLayoutId, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/delete-page-layout-widget-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/delete-page-layout-widget-operation-factory.util.ts deleted file mode 100644 index 1a3e1d2f196..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/delete-page-layout-widget-operation-factory.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_WIDGET_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -type DeletePageLayoutWidgetOperationFactoryParams = { - gqlFields?: string; - pageLayoutWidgetId: string; -}; - -export const deletePageLayoutWidgetOperationFactory = ({ - gqlFields = PAGE_LAYOUT_WIDGET_GQL_FIELDS, - pageLayoutWidgetId, -}: DeletePageLayoutWidgetOperationFactoryParams) => ({ - query: gql` - mutation DeletePageLayoutWidget($id: String!) { - deletePageLayoutWidget(id: $id) { - ${gqlFields} - } - } - `, - variables: { - id: pageLayoutWidgetId, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/find-page-layout-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/find-page-layout-operation-factory.util.ts deleted file mode 100644 index fa3cdce1727..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/find-page-layout-operation-factory.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -type FindPageLayoutOperationFactoryParams = { - gqlFields?: string; - pageLayoutId: string; -}; - -export const findPageLayoutOperationFactory = ({ - gqlFields = PAGE_LAYOUT_GQL_FIELDS, - pageLayoutId, -}: FindPageLayoutOperationFactoryParams) => ({ - query: gql` - query GetPageLayout($id: String!) { - getPageLayout(id: $id) { - ${gqlFields} - } - } - `, - variables: { - id: pageLayoutId, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/find-page-layout-tab-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/find-page-layout-tab-operation-factory.util.ts deleted file mode 100644 index 991f5023b4f..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/find-page-layout-tab-operation-factory.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_TAB_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -type FindPageLayoutTabOperationFactoryParams = { - gqlFields?: string; - pageLayoutTabId: string; -}; - -export const findPageLayoutTabOperationFactory = ({ - gqlFields = PAGE_LAYOUT_TAB_GQL_FIELDS, - pageLayoutTabId, -}: FindPageLayoutTabOperationFactoryParams) => ({ - query: gql` - query GetPageLayoutTab($id: String!) { - getPageLayoutTab(id: $id) { - ${gqlFields} - } - } - `, - variables: { - id: pageLayoutTabId, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/find-page-layout-tabs-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/find-page-layout-tabs-operation-factory.util.ts deleted file mode 100644 index 03d045392dc..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/find-page-layout-tabs-operation-factory.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_TAB_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -type FindPageLayoutTabsOperationFactoryParams = { - gqlFields?: string; - pageLayoutId: string; -}; - -export const findPageLayoutTabsOperationFactory = ({ - gqlFields = PAGE_LAYOUT_TAB_GQL_FIELDS, - pageLayoutId, -}: FindPageLayoutTabsOperationFactoryParams) => ({ - query: gql` - query GetPageLayoutTabs($pageLayoutId: String!) { - getPageLayoutTabs(pageLayoutId: $pageLayoutId) { - ${gqlFields} - } - } - `, - variables: { - pageLayoutId, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/find-page-layout-widget-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/find-page-layout-widget-operation-factory.util.ts deleted file mode 100644 index d36d371f68d..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/find-page-layout-widget-operation-factory.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_WIDGET_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -type FindPageLayoutWidgetOperationFactoryParams = { - gqlFields?: string; - pageLayoutWidgetId: string; -}; - -export const findPageLayoutWidgetOperationFactory = ({ - gqlFields = PAGE_LAYOUT_WIDGET_GQL_FIELDS, - pageLayoutWidgetId, -}: FindPageLayoutWidgetOperationFactoryParams) => ({ - query: gql` - query GetPageLayoutWidget($id: String!) { - getPageLayoutWidget(id: $id) { - ${gqlFields} - } - } - `, - variables: { - id: pageLayoutWidgetId, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/find-page-layout-widgets-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/find-page-layout-widgets-operation-factory.util.ts deleted file mode 100644 index 45dfdafa8fa..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/find-page-layout-widgets-operation-factory.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_WIDGET_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -type FindPageLayoutWidgetsOperationFactoryParams = { - gqlFields?: string; - pageLayoutTabId: string; -}; - -export const findPageLayoutWidgetsOperationFactory = ({ - gqlFields = PAGE_LAYOUT_WIDGET_GQL_FIELDS, - pageLayoutTabId, -}: FindPageLayoutWidgetsOperationFactoryParams) => ({ - query: gql` - query GetPageLayoutWidgets($pageLayoutTabId: String!) { - getPageLayoutWidgets(pageLayoutTabId: $pageLayoutTabId) { - ${gqlFields} - } - } - `, - variables: { - pageLayoutTabId, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/find-page-layouts-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/find-page-layouts-operation-factory.util.ts deleted file mode 100644 index f47c9ccc1f2..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/find-page-layouts-operation-factory.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -type FindPageLayoutsOperationFactoryParams = { - gqlFields?: string; - objectMetadataId?: string; -}; - -export const findPageLayoutsOperationFactory = ({ - gqlFields = PAGE_LAYOUT_GQL_FIELDS, - objectMetadataId, -}: FindPageLayoutsOperationFactoryParams = {}) => ({ - query: gql` - query GetPageLayouts($objectMetadataId: String) { - getPageLayouts(objectMetadataId: $objectMetadataId) { - ${gqlFields} - } - } - `, - variables: { - objectMetadataId, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/page-layout-graphql.util.ts b/packages/twenty-server/test/integration/graphql/utils/page-layout-graphql.util.ts deleted file mode 100644 index 13a03b5910f..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/page-layout-graphql.util.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { type GraphQLResponse } from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; - -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 { createPageLayoutOperationFactory } from './create-page-layout-operation-factory.util'; -import { destroyPageLayoutOperationFactory } from './destroy-page-layout-operation-factory.util'; -import { findPageLayoutsOperationFactory } from './find-page-layouts-operation-factory.util'; - -interface CreatePageLayoutResponse extends Record { - createPageLayout: PageLayoutEntity; -} - -export const createTestPageLayoutWithGraphQL = async ( - data: { - name: string; - type?: PageLayoutType; - objectMetadataId?: string; - } = { name: 'Test Page Layout' }, -): Promise => { - const operation = createPageLayoutOperationFactory({ - data: { - name: data.name, - type: data.type || PageLayoutType.RECORD_PAGE, - objectMetadataId: data.objectMetadataId, - }, - }); - - const response = (await makeGraphqlAPIRequest( - operation, - )) as GraphQLResponse; - - if (response.body.errors) { - throw new Error( - `Failed to create test page layout: ${JSON.stringify(response.body.errors)}`, - ); - } - - if (!response.body.data) { - throw new Error('No data returned from createTestPageLayoutWithGraphQL'); - } - - return response.body.data.createPageLayout; -}; - -export const cleanupPageLayoutRecordsWithGraphQL = async (): Promise => { - const operation = findPageLayoutsOperationFactory(); - const response = await makeGraphqlAPIRequest(operation); - - if (response.body.data?.getPageLayouts) { - for (const pageLayout of response.body.data.getPageLayouts) { - const destroyOperation = destroyPageLayoutOperationFactory({ - pageLayoutId: pageLayout.id, - }); - - await makeGraphqlAPIRequest(destroyOperation); - } - } -}; diff --git a/packages/twenty-server/test/integration/graphql/utils/page-layout-tab-graphql.util.ts b/packages/twenty-server/test/integration/graphql/utils/page-layout-tab-graphql.util.ts deleted file mode 100644 index 5c4ef4a628b..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/page-layout-tab-graphql.util.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { type GraphQLResponse } from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; - -import { type PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity'; - -import { createPageLayoutTabOperationFactory } from './create-page-layout-tab-operation-factory.util'; -import { destroyPageLayoutTabOperationFactory } from './destroy-page-layout-tab-operation-factory.util'; -import { findPageLayoutTabsOperationFactory } from './find-page-layout-tabs-operation-factory.util'; - -interface CreatePageLayoutTabResponse extends Record { - createPageLayoutTab: PageLayoutTabEntity; -} - -export const createTestPageLayoutTabWithGraphQL = async (data: { - title: string; - position?: number; - pageLayoutId: string; -}): Promise => { - const operation = createPageLayoutTabOperationFactory({ - data: { - title: data.title, - position: data.position || 0, - pageLayoutId: data.pageLayoutId, - }, - }); - - const response = (await makeGraphqlAPIRequest( - operation, - )) as GraphQLResponse; - - if (response.body.errors) { - throw new Error( - `Failed to create test page layout tab: ${JSON.stringify(response.body.errors)}`, - ); - } - - if (!response.body.data) { - throw new Error('No data returned from createTestPageLayoutTabWithGraphQL'); - } - - return response.body.data.createPageLayoutTab; -}; - -export const cleanupPageLayoutTabRecordsWithGraphQL = async ( - pageLayoutId: string, -): Promise => { - const operation = findPageLayoutTabsOperationFactory({ pageLayoutId }); - const response = await makeGraphqlAPIRequest(operation); - - if (response.body.data?.getPageLayoutTabs) { - for (const pageLayoutTab of response.body.data.getPageLayoutTabs) { - const destroyOperation = destroyPageLayoutTabOperationFactory({ - pageLayoutTabId: pageLayoutTab.id, - }); - - await makeGraphqlAPIRequest(destroyOperation); - } - } -}; diff --git a/packages/twenty-server/test/integration/graphql/utils/page-layout-widget-graphql.util.ts b/packages/twenty-server/test/integration/graphql/utils/page-layout-widget-graphql.util.ts deleted file mode 100644 index 452ff1e63b7..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/page-layout-widget-graphql.util.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { type GraphQLResponse } from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; - -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 { createPageLayoutWidgetOperationFactory } from './create-page-layout-widget-operation-factory.util'; -import { destroyPageLayoutWidgetOperationFactory } from './destroy-page-layout-widget-operation-factory.util'; -import { findPageLayoutWidgetsOperationFactory } from './find-page-layout-widgets-operation-factory.util'; - -interface CreatePageLayoutWidgetResponse extends Record { - createPageLayoutWidget: PageLayoutWidgetEntity; -} - -export const createTestPageLayoutWidgetWithGraphQL = async (data: { - title: string; - type?: WidgetType; - pageLayoutTabId: string; - objectMetadataId?: string | null; - gridPosition?: { - row: number; - column: number; - rowSpan: number; - columnSpan: number; - }; - configuration?: Record | null; -}): Promise => { - const operation = createPageLayoutWidgetOperationFactory({ - data: { - title: data.title, - type: data.type || WidgetType.VIEW, - pageLayoutTabId: data.pageLayoutTabId, - objectMetadataId: data.objectMetadataId || null, - gridPosition: data.gridPosition || { - row: 0, - column: 0, - rowSpan: 1, - columnSpan: 1, - }, - configuration: data.configuration || null, - }, - }); - - const response = (await makeGraphqlAPIRequest( - operation, - )) as GraphQLResponse; - - if (response.body.errors) { - throw new Error( - `Failed to create test page layout widget: ${JSON.stringify(response.body.errors)}`, - ); - } - - if (!response.body.data) { - throw new Error( - 'No data returned from createTestPageLayoutWidgetWithGraphQL', - ); - } - - return response.body.data.createPageLayoutWidget; -}; - -export const cleanupPageLayoutWidgetRecordsWithGraphQL = async ( - pageLayoutTabId: string, -): Promise => { - const operation = findPageLayoutWidgetsOperationFactory({ pageLayoutTabId }); - const response = await makeGraphqlAPIRequest(operation); - - if (response.body.data?.getPageLayoutWidgets) { - for (const pageLayoutWidget of response.body.data.getPageLayoutWidgets) { - const destroyOperation = destroyPageLayoutWidgetOperationFactory({ - pageLayoutWidgetId: pageLayoutWidget.id, - }); - - await makeGraphqlAPIRequest(destroyOperation); - } - } -}; diff --git a/packages/twenty-server/test/integration/graphql/utils/restore-page-layout-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/restore-page-layout-operation-factory.util.ts deleted file mode 100644 index 72fc8906d69..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/restore-page-layout-operation-factory.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -type RestorePageLayoutOperationFactoryParams = { - gqlFields?: string; - pageLayoutId: string; -}; - -export const restorePageLayoutOperationFactory = ({ - gqlFields = PAGE_LAYOUT_GQL_FIELDS, - pageLayoutId, -}: RestorePageLayoutOperationFactoryParams) => ({ - query: gql` - mutation RestorePageLayout($id: String!) { - restorePageLayout(id: $id) { - ${gqlFields} - } - } - `, - variables: { - id: pageLayoutId, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/restore-page-layout-tab-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/restore-page-layout-tab-operation-factory.util.ts deleted file mode 100644 index f547ad29525..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/restore-page-layout-tab-operation-factory.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_TAB_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -type RestorePageLayoutTabOperationFactoryParams = { - gqlFields?: string; - pageLayoutTabId: string; -}; - -export const restorePageLayoutTabOperationFactory = ({ - gqlFields = PAGE_LAYOUT_TAB_GQL_FIELDS, - pageLayoutTabId, -}: RestorePageLayoutTabOperationFactoryParams) => ({ - query: gql` - mutation RestorePageLayoutTab($id: String!) { - restorePageLayoutTab(id: $id) { - ${gqlFields} - } - } - `, - variables: { - id: pageLayoutTabId, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/restore-page-layout-widget-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/restore-page-layout-widget-operation-factory.util.ts deleted file mode 100644 index 49e083adb9f..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/restore-page-layout-widget-operation-factory.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_WIDGET_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -type RestorePageLayoutWidgetOperationFactoryParams = { - gqlFields?: string; - pageLayoutWidgetId: string; -}; - -export const restorePageLayoutWidgetOperationFactory = ({ - gqlFields = PAGE_LAYOUT_WIDGET_GQL_FIELDS, - pageLayoutWidgetId, -}: RestorePageLayoutWidgetOperationFactoryParams) => ({ - query: gql` - mutation RestorePageLayoutWidget($id: String!) { - restorePageLayoutWidget(id: $id) { - ${gqlFields} - } - } - `, - variables: { - id: pageLayoutWidgetId, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/update-page-layout-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-page-layout-operation-factory.util.ts deleted file mode 100644 index 38384d096f8..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/update-page-layout-operation-factory.util.ts +++ /dev/null @@ -1,28 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -import { type UpdatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout.input'; - -type UpdatePageLayoutOperationFactoryParams = { - gqlFields?: string; - pageLayoutId: string; - data: UpdatePageLayoutInput; -}; - -export const updatePageLayoutOperationFactory = ({ - gqlFields = PAGE_LAYOUT_GQL_FIELDS, - pageLayoutId, - data, -}: UpdatePageLayoutOperationFactoryParams) => ({ - query: gql` - mutation UpdatePageLayout($id: String!, $input: UpdatePageLayoutInput!) { - updatePageLayout(id: $id, input: $input) { - ${gqlFields} - } - } - `, - variables: { - id: pageLayoutId, - input: data, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/update-page-layout-tab-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-page-layout-tab-operation-factory.util.ts deleted file mode 100644 index 92a6e7ab026..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/update-page-layout-tab-operation-factory.util.ts +++ /dev/null @@ -1,28 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_TAB_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -import { type UpdatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-tab.input'; - -type UpdatePageLayoutTabOperationFactoryParams = { - gqlFields?: string; - pageLayoutTabId: string; - data: UpdatePageLayoutTabInput; -}; - -export const updatePageLayoutTabOperationFactory = ({ - gqlFields = PAGE_LAYOUT_TAB_GQL_FIELDS, - pageLayoutTabId, - data, -}: UpdatePageLayoutTabOperationFactoryParams) => ({ - query: gql` - mutation UpdatePageLayoutTab($id: String!, $input: UpdatePageLayoutTabInput!) { - updatePageLayoutTab(id: $id, input: $input) { - ${gqlFields} - } - } - `, - variables: { - id: pageLayoutTabId, - input: data, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/update-page-layout-widget-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-page-layout-widget-operation-factory.util.ts deleted file mode 100644 index 4b7f1c18d99..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/update-page-layout-widget-operation-factory.util.ts +++ /dev/null @@ -1,28 +0,0 @@ -import gql from 'graphql-tag'; -import { PAGE_LAYOUT_WIDGET_GQL_FIELDS } from 'test/integration/constants/page-layout-gql-fields.constants'; - -import { type UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-widget.input'; - -type UpdatePageLayoutWidgetOperationFactoryParams = { - gqlFields?: string; - pageLayoutWidgetId: string; - data: UpdatePageLayoutWidgetInput; -}; - -export const updatePageLayoutWidgetOperationFactory = ({ - gqlFields = PAGE_LAYOUT_WIDGET_GQL_FIELDS, - pageLayoutWidgetId, - data, -}: UpdatePageLayoutWidgetOperationFactoryParams) => ({ - query: gql` - mutation UpdatePageLayoutWidget($id: String!, $input: UpdatePageLayoutWidgetInput!) { - updatePageLayoutWidget(id: $id, input: $input) { - ${gqlFields} - } - } - `, - variables: { - id: pageLayoutWidgetId, - input: data, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/update-page-layout-with-tabs-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-page-layout-with-tabs-operation-factory.util.ts deleted file mode 100644 index 322c94856b3..00000000000 --- a/packages/twenty-server/test/integration/graphql/utils/update-page-layout-with-tabs-operation-factory.util.ts +++ /dev/null @@ -1,53 +0,0 @@ -import gql from 'graphql-tag'; -import { - PAGE_LAYOUT_GQL_FIELDS, - PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS, -} from 'test/integration/constants/page-layout-gql-fields.constants'; - -import { type UpdatePageLayoutWithTabsInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input'; - -type UpdatePageLayoutWithTabsOperationFactoryParams = { - gqlFields?: string; - pageLayoutId: string; - data: UpdatePageLayoutWithTabsInput; -}; - -export const updatePageLayoutWithTabsOperationFactory = ({ - gqlFields = PAGE_LAYOUT_GQL_FIELDS, - pageLayoutId, - data, -}: UpdatePageLayoutWithTabsOperationFactoryParams) => ({ - query: gql` - mutation UpdatePageLayoutWithTabsAndWidgets($id: String!, $input: UpdatePageLayoutWithTabsInput!) { - updatePageLayoutWithTabsAndWidgets(id: $id, input: $input) { - ${gqlFields} - tabs { - id - title - position - pageLayoutId - widgets { - id - title - type - pageLayoutTabId - objectMetadataId - gridPosition { - row - column - rowSpan - columnSpan - } - configuration { - ${PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS} - } - } - } - } - } - `, - variables: { - id: pageLayoutId, - input: data, - }, -}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/failing-page-layout-tab-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/failing-page-layout-tab-creation.integration-spec.ts.snap new file mode 100644 index 00000000000..7f9349e2d5c --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/failing-page-layout-tab-creation.integration-spec.ts.snap @@ -0,0 +1,82 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout tab creation should fail when pageLayoutId references non-existent layout 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "agent": [], + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "pageLayout": [], + "pageLayoutTab": [ + { + "errors": [ + { + "code": "PAGE_LAYOUT_NOT_FOUND", + "message": "Page layout not found", + "userFriendlyMessage": "Page layout not found", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "title": "Tab With Non-Existent Layout", + }, + "status": "fail", + "type": "create_page_layout_tab", + }, + ], + "pageLayoutWidget": [], + "role": [], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidAgent": 0, + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidPageLayout": 0, + "invalidPageLayoutTab": 0, + "invalidPageLayoutWidget": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating page layout tab", + "name": "GraphQLError", +} +`; + +exports[`Page layout tab creation should fail when title is missing 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "http": { + "status": 400, + }, + "userFriendlyMessage": "An error occurred.", + }, + "message": "Field "title" of required type "String!" was not provided.", + "name": "GraphQLError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/failing-page-layout-tab-deletion.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/failing-page-layout-tab-deletion.integration-spec.ts.snap new file mode 100644 index 00000000000..b842360db54 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/failing-page-layout-tab-deletion.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout tab deletion should fail when deleting a non-existent page layout tab 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Page layout tab to delete not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/failing-page-layout-tab-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/failing-page-layout-tab-update.integration-spec.ts.snap new file mode 100644 index 00000000000..02654710ec5 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/failing-page-layout-tab-update.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout tab update should fail when updating a non-existent page layout tab 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Page layout tab to update not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/successful-page-layout-tab-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/successful-page-layout-tab-creation.integration-spec.ts.snap new file mode 100644 index 00000000000..917edea2ea2 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/successful-page-layout-tab-creation.integration-spec.ts.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout tab creation should succeed should create a page layout tab 1`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "pageLayoutId": Any, + "position": 0, + "title": "Test Tab", + "updatedAt": Any, + "widgets": null, +} +`; + +exports[`Page layout tab creation should succeed should create a page layout tab with position 1`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "pageLayoutId": Any, + "position": 5, + "title": "Positioned Tab", + "updatedAt": Any, + "widgets": null, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/successful-page-layout-tab-deletion.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/successful-page-layout-tab-deletion.integration-spec.ts.snap new file mode 100644 index 00000000000..a1ce3169a52 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/successful-page-layout-tab-deletion.integration-spec.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout tab deletion should succeed should soft delete and restore a page layout tab 1`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "pageLayoutId": Any, + "position": 0, + "title": "Tab To Delete", + "updatedAt": Any, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/successful-page-layout-tab-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/successful-page-layout-tab-update.integration-spec.ts.snap new file mode 100644 index 00000000000..03d10eca272 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/__snapshots__/successful-page-layout-tab-update.integration-spec.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout tab update should succeed should update page layout tab position 1`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "pageLayoutId": Any, + "position": 10, + "title": "Original Tab Title", + "updatedAt": Any, +} +`; + +exports[`Page layout tab update should succeed should update page layout tab title 1`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "pageLayoutId": Any, + "position": 0, + "title": "Updated Tab Title", + "updatedAt": Any, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/failing-page-layout-tab-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/failing-page-layout-tab-creation.integration-spec.ts new file mode 100644 index 00000000000..091e7afc1e0 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/failing-page-layout-tab-creation.integration-spec.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util'; +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; +import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; + +import { type CreatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout-tab.input'; + +describe('Page layout tab creation should fail', () => { + let testPageLayoutId: string; + + beforeAll(async () => { + const { data } = await createOnePageLayout({ + expectToFail: false, + input: { name: 'Test Page Layout For Tab Creation Failures' }, + }); + + testPageLayoutId = data.createPageLayout.id; + }); + + afterAll(async () => { + await destroyOnePageLayout({ + expectToFail: false, + input: { id: testPageLayoutId }, + }); + }); + + it('when title is missing', async () => { + const { errors } = await createOnePageLayoutTab({ + expectToFail: true, + input: { pageLayoutId: testPageLayoutId } as CreatePageLayoutTabInput, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); + + it('when pageLayoutId references non-existent layout', async () => { + const { errors } = await createOnePageLayoutTab({ + expectToFail: true, + input: { + title: 'Tab With Non-Existent Layout', + pageLayoutId: faker.string.uuid(), + }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/failing-page-layout-tab-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/failing-page-layout-tab-deletion.integration-spec.ts new file mode 100644 index 00000000000..a6915b5e7ea --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/failing-page-layout-tab-deletion.integration-spec.ts @@ -0,0 +1,14 @@ +import { faker } from '@faker-js/faker'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { deleteOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/delete-one-page-layout-tab.util'; + +describe('Page layout tab deletion should fail', () => { + it('when deleting a non-existent page layout tab', async () => { + const { errors } = await deleteOnePageLayoutTab({ + expectToFail: true, + input: { id: faker.string.uuid() }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/failing-page-layout-tab-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/failing-page-layout-tab-update.integration-spec.ts new file mode 100644 index 00000000000..869a846cc4c --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/failing-page-layout-tab-update.integration-spec.ts @@ -0,0 +1,17 @@ +import { faker } from '@faker-js/faker'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { updateOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/update-one-page-layout-tab.util'; + +describe('Page layout tab update should fail', () => { + it('when updating a non-existent page layout tab', async () => { + const { errors } = await updateOnePageLayoutTab({ + expectToFail: true, + input: { + id: faker.string.uuid(), + title: 'Updated Title', + }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/successful-page-layout-tab-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/successful-page-layout-tab-creation.integration-spec.ts new file mode 100644 index 00000000000..38225f5312f --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/successful-page-layout-tab-creation.integration-spec.ts @@ -0,0 +1,86 @@ +import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util'; +import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util'; +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; +import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; + +type TestContext = { + input: { + title: string; + position?: number; + }; +}; + +const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [ + { + title: 'create a page layout tab', + context: { + input: { + title: 'Test Tab', + }, + }, + }, + { + title: 'create a page layout tab with position', + context: { + input: { + title: 'Positioned Tab', + position: 5, + }, + }, + }, +]; + +describe('Page layout tab creation should succeed', () => { + let testPageLayoutId: string; + let createdPageLayoutTabId: string; + + beforeAll(async () => { + const { data } = await createOnePageLayout({ + expectToFail: false, + input: { name: 'Test Page Layout For Tabs' }, + }); + + testPageLayoutId = data.createPageLayout.id; + }); + + afterAll(async () => { + await destroyOnePageLayout({ + expectToFail: false, + input: { id: testPageLayoutId }, + }); + }); + + afterEach(async () => { + if (createdPageLayoutTabId) { + await destroyOnePageLayoutTab({ + expectToFail: false, + input: { id: createdPageLayoutTabId }, + }); + createdPageLayoutTabId = ''; + } + }); + + it.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))( + 'should $title', + async ({ context: { input } }) => { + const { data } = await createOnePageLayoutTab({ + expectToFail: false, + input: { + ...input, + pageLayoutId: testPageLayoutId, + }, + }); + + createdPageLayoutTabId = data?.createPageLayoutTab?.id; + + expect(data.createPageLayoutTab).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ ...data.createPageLayoutTab }), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/successful-page-layout-tab-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/successful-page-layout-tab-deletion.integration-spec.ts new file mode 100644 index 00000000000..c18fc70aa19 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/successful-page-layout-tab-deletion.integration-spec.ts @@ -0,0 +1,100 @@ +import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util'; +import { deleteOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/delete-one-page-layout-tab.util'; +import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util'; +import { restoreOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/restore-one-page-layout-tab.util'; +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; +import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; + +type TestContext = { + title: string; + operation: 'soft-delete-restore' | 'hard-delete'; +}; + +const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [ + { + title: 'soft delete and restore a page layout tab', + context: { + title: 'Tab To Delete', + operation: 'soft-delete-restore', + }, + }, + { + title: 'hard delete a page layout tab', + context: { + title: 'Tab To Destroy', + operation: 'hard-delete', + }, + }, +]; + +describe('Page layout tab deletion should succeed', () => { + let testPageLayoutId: string; + + beforeAll(async () => { + const { data } = await createOnePageLayout({ + expectToFail: false, + input: { name: 'Test Page Layout For Tab Deletions' }, + }); + + testPageLayoutId = data.createPageLayout.id; + }); + + afterAll(async () => { + await destroyOnePageLayout({ + expectToFail: false, + input: { id: testPageLayoutId }, + }); + }); + + it.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))( + 'should $title', + async ({ context: { title, operation } }) => { + const { data: createData } = await createOnePageLayoutTab({ + expectToFail: false, + input: { + title, + pageLayoutId: testPageLayoutId, + }, + }); + + const tabId = createData.createPageLayoutTab.id; + + if (operation === 'soft-delete-restore') { + const { data: deleteData } = await deleteOnePageLayoutTab({ + expectToFail: false, + input: { id: tabId }, + }); + + expect(deleteData.deletePageLayoutTab).toBe(true); + + const { data: restoreData } = await restoreOnePageLayoutTab({ + expectToFail: false, + input: { id: tabId }, + }); + + expect(restoreData.restorePageLayoutTab).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ + ...restoreData.restorePageLayoutTab, + }), + ); + + await destroyOnePageLayoutTab({ + expectToFail: false, + input: { id: tabId }, + }); + } else { + const { data: destroyData } = await destroyOnePageLayoutTab({ + expectToFail: false, + input: { id: tabId }, + }); + + expect(destroyData.destroyPageLayoutTab).toBe(true); + } + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/successful-page-layout-tab-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/successful-page-layout-tab-update.integration-spec.ts new file mode 100644 index 00000000000..8da8c76f7d7 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/successful-page-layout-tab-update.integration-spec.ts @@ -0,0 +1,93 @@ +import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util'; +import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util'; +import { updateOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/update-one-page-layout-tab.util'; +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; +import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; + +type TestContext = { + input: { + title?: string; + position?: number; + }; +}; + +const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [ + { + title: 'update page layout tab title', + context: { + input: { + title: 'Updated Tab Title', + }, + }, + }, + { + title: 'update page layout tab position', + context: { + input: { + position: 10, + }, + }, + }, +]; + +describe('Page layout tab update should succeed', () => { + let testPageLayoutId: string; + let testPageLayoutTabId: string; + + beforeAll(async () => { + const { data } = await createOnePageLayout({ + expectToFail: false, + input: { name: 'Test Page Layout For Tab Updates' }, + }); + + testPageLayoutId = data.createPageLayout.id; + }); + + afterAll(async () => { + await destroyOnePageLayout({ + expectToFail: false, + input: { id: testPageLayoutId }, + }); + }); + + beforeEach(async () => { + const { data } = await createOnePageLayoutTab({ + expectToFail: false, + input: { + title: 'Original Tab Title', + pageLayoutId: testPageLayoutId, + }, + }); + + testPageLayoutTabId = data.createPageLayoutTab.id; + }); + + afterEach(async () => { + await destroyOnePageLayoutTab({ + expectToFail: false, + input: { id: testPageLayoutTabId }, + }); + }); + + it.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))( + 'should $title', + async ({ context: { input } }) => { + const { data } = await updateOnePageLayoutTab({ + expectToFail: false, + input: { + id: testPageLayoutTabId, + ...input, + }, + }); + + expect(data.updatePageLayoutTab).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ ...data.updatePageLayoutTab }), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab-query-factory.util.ts new file mode 100644 index 00000000000..e5898861af1 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab-query-factory.util.ts @@ -0,0 +1,52 @@ +import gql from 'graphql-tag'; +import { WIDGET_CONFIGURATION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type CreatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout-tab.input'; + +export type CreateOnePageLayoutTabFactoryInput = CreatePageLayoutTabInput; + +const DEFAULT_PAGE_LAYOUT_TAB_GQL_FIELDS = ` + id + title + position + pageLayoutId + createdAt + updatedAt + deletedAt + widgets { + id + title + type + pageLayoutTabId + objectMetadataId + gridPosition { + row + column + rowSpan + columnSpan + } + configuration { + ${WIDGET_CONFIGURATION_GQL_FIELDS} + } + createdAt + updatedAt + deletedAt + } +`; + +export const createOnePageLayoutTabQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_TAB_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation CreatePageLayoutTab($input: CreatePageLayoutTabInput!) { + createPageLayoutTab(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util.ts new file mode 100644 index 00000000000..670cb4cda80 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util.ts @@ -0,0 +1,43 @@ +import { + type CreateOnePageLayoutTabFactoryInput, + createOnePageLayoutTabQueryFactory, +} from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-tab.dto'; + +export const createOnePageLayoutTab = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + createPageLayoutTab: PageLayoutTabDTO; +}> => { + const graphqlOperation = createOnePageLayoutTabQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout tab creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout tab creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/delete-one-page-layout-tab-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/delete-one-page-layout-tab-query-factory.util.ts new file mode 100644 index 00000000000..15ea71b91c3 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/delete-one-page-layout-tab-query-factory.util.ts @@ -0,0 +1,19 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type DeleteOnePageLayoutTabFactoryInput = { + id: string; +}; + +export const deleteOnePageLayoutTabQueryFactory = ({ + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation DeletePageLayoutTab($id: String!) { + deletePageLayoutTab(id: $id) + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/delete-one-page-layout-tab.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/delete-one-page-layout-tab.util.ts new file mode 100644 index 00000000000..f48411bf17d --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/delete-one-page-layout-tab.util.ts @@ -0,0 +1,39 @@ +import { + type DeleteOnePageLayoutTabFactoryInput, + deleteOnePageLayoutTabQueryFactory, +} from 'test/integration/metadata/suites/page-layout-tab/utils/delete-one-page-layout-tab-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +export const deleteOnePageLayoutTab = async ({ + input, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + deletePageLayoutTab: boolean; +}> => { + const graphqlOperation = deleteOnePageLayoutTabQueryFactory({ + input, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout tab deletion should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout tab deletion has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab-query-factory.util.ts new file mode 100644 index 00000000000..cfc673a30f9 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab-query-factory.util.ts @@ -0,0 +1,19 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type DestroyOnePageLayoutTabFactoryInput = { + id: string; +}; + +export const destroyOnePageLayoutTabQueryFactory = ({ + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation DestroyPageLayoutTab($id: String!) { + destroyPageLayoutTab(id: $id) + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util.ts new file mode 100644 index 00000000000..9553d11d4e7 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util.ts @@ -0,0 +1,39 @@ +import { + type DestroyOnePageLayoutTabFactoryInput, + destroyOnePageLayoutTabQueryFactory, +} from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +export const destroyOnePageLayoutTab = async ({ + input, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + destroyPageLayoutTab: boolean; +}> => { + const graphqlOperation = destroyOnePageLayoutTabQueryFactory({ + input, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout tab destroy should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout tab destroy has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/find-one-page-layout-tab-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/find-one-page-layout-tab-query-factory.util.ts new file mode 100644 index 00000000000..9e68ca4a8e7 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/find-one-page-layout-tab-query-factory.util.ts @@ -0,0 +1,49 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type FindOnePageLayoutTabFactoryInput = { + id: string; +}; + +const DEFAULT_PAGE_LAYOUT_TAB_GQL_FIELDS = ` + id + title + position + pageLayoutId + createdAt + updatedAt + deletedAt + widgets { + id + title + type + pageLayoutTabId + objectMetadataId + gridPosition { + row + column + rowSpan + columnSpan + } + configuration + createdAt + updatedAt + deletedAt + } +`; + +export const findOnePageLayoutTabQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_TAB_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + query GetPageLayoutTab($id: String!) { + getPageLayoutTab(id: $id) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/find-one-page-layout-tab.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/find-one-page-layout-tab.util.ts new file mode 100644 index 00000000000..4e764d1d30c --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/find-one-page-layout-tab.util.ts @@ -0,0 +1,43 @@ +import { + type FindOnePageLayoutTabFactoryInput, + findOnePageLayoutTabQueryFactory, +} from 'test/integration/metadata/suites/page-layout-tab/utils/find-one-page-layout-tab-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-tab.dto'; + +export const findOnePageLayoutTab = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + getPageLayoutTab: PageLayoutTabDTO; +}> => { + const graphqlOperation = findOnePageLayoutTabQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Find page layout tab should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Find page layout tab has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/find-page-layout-tabs-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/find-page-layout-tabs-query-factory.util.ts new file mode 100644 index 00000000000..f7446b953b9 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/find-page-layout-tabs-query-factory.util.ts @@ -0,0 +1,32 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type FindPageLayoutTabsFactoryInput = { + pageLayoutId: string; +}; + +const DEFAULT_PAGE_LAYOUT_TAB_GQL_FIELDS = ` + id + title + position + pageLayoutId + createdAt + updatedAt + deletedAt +`; + +export const findPageLayoutTabsQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_TAB_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + query GetPageLayoutTabs($pageLayoutId: String!) { + getPageLayoutTabs(pageLayoutId: $pageLayoutId) { + ${gqlFields} + } + } + `, + variables: { + pageLayoutId: input.pageLayoutId, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/find-page-layout-tabs.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/find-page-layout-tabs.util.ts new file mode 100644 index 00000000000..6a0775a074b --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/find-page-layout-tabs.util.ts @@ -0,0 +1,43 @@ +import { + type FindPageLayoutTabsFactoryInput, + findPageLayoutTabsQueryFactory, +} from 'test/integration/metadata/suites/page-layout-tab/utils/find-page-layout-tabs-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-tab.dto'; + +export const findPageLayoutTabs = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + getPageLayoutTabs: PageLayoutTabDTO[]; +}> => { + const graphqlOperation = findPageLayoutTabsQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Find page layout tabs should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Find page layout tabs has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/restore-one-page-layout-tab-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/restore-one-page-layout-tab-query-factory.util.ts new file mode 100644 index 00000000000..e9646cd70d8 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/restore-one-page-layout-tab-query-factory.util.ts @@ -0,0 +1,32 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type RestoreOnePageLayoutTabFactoryInput = { + id: string; +}; + +const DEFAULT_PAGE_LAYOUT_TAB_GQL_FIELDS = ` + id + title + position + pageLayoutId + createdAt + updatedAt + deletedAt +`; + +export const restoreOnePageLayoutTabQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_TAB_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation RestorePageLayoutTab($id: String!) { + restorePageLayoutTab(id: $id) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/restore-one-page-layout-tab.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/restore-one-page-layout-tab.util.ts new file mode 100644 index 00000000000..383e1d61b34 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/restore-one-page-layout-tab.util.ts @@ -0,0 +1,43 @@ +import { + type RestoreOnePageLayoutTabFactoryInput, + restoreOnePageLayoutTabQueryFactory, +} from 'test/integration/metadata/suites/page-layout-tab/utils/restore-one-page-layout-tab-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-tab.dto'; + +export const restoreOnePageLayoutTab = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + restorePageLayoutTab: PageLayoutTabDTO; +}> => { + const graphqlOperation = restoreOnePageLayoutTabQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout tab restore should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout tab restore has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/update-one-page-layout-tab-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/update-one-page-layout-tab-query-factory.util.ts new file mode 100644 index 00000000000..0b737dd4566 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/update-one-page-layout-tab-query-factory.util.ts @@ -0,0 +1,38 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type UpdatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-tab.input'; + +export type UpdateOnePageLayoutTabFactoryInput = { + id: string; +} & UpdatePageLayoutTabInput; + +const DEFAULT_PAGE_LAYOUT_TAB_GQL_FIELDS = ` + id + title + position + pageLayoutId + createdAt + updatedAt + deletedAt +`; + +export const updateOnePageLayoutTabQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_TAB_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation UpdatePageLayoutTab($id: String!, $input: UpdatePageLayoutTabInput!) { + updatePageLayoutTab(id: $id, input: $input) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + input: { + title: input.title, + position: input.position, + }, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/update-one-page-layout-tab.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/update-one-page-layout-tab.util.ts new file mode 100644 index 00000000000..a339fa2823e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-tab/utils/update-one-page-layout-tab.util.ts @@ -0,0 +1,43 @@ +import { + type UpdateOnePageLayoutTabFactoryInput, + updateOnePageLayoutTabQueryFactory, +} from 'test/integration/metadata/suites/page-layout-tab/utils/update-one-page-layout-tab-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-tab.dto'; + +export const updateOnePageLayoutTab = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + updatePageLayoutTab: PageLayoutTabDTO; +}> => { + const graphqlOperation = updateOnePageLayoutTabQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout tab update should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout tab update has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-creation.integration-spec.ts.snap new file mode 100644 index 00000000000..2041bf4bb57 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-creation.integration-spec.ts.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout widget creation should fail when gridPosition has invalid values 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", + }, + "message": "row must not be less than 0", + "name": "UserInputError", +} +`; + +exports[`Page layout widget creation should fail when pageLayoutTabId references non-existent tab 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "agent": [], + "cronTrigger": [], + "databaseEventTrigger": [], + "fieldMetadata": [], + "index": [], + "objectMetadata": [], + "pageLayout": [], + "pageLayoutTab": [], + "pageLayoutWidget": [ + { + "errors": [ + { + "code": "PAGE_LAYOUT_TAB_NOT_FOUND", + "message": "Page layout tab not found", + "userFriendlyMessage": "Page layout tab not found", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "pageLayoutTabId": Any, + }, + "status": "fail", + "type": "create_page_layout_widget", + }, + ], + "role": [], + "roleTarget": [], + "routeTrigger": [], + "serverlessFunction": [], + "view": [], + "viewField": [], + "viewFilter": [], + "viewGroup": [], + }, + "message": "Validation failed for 0 object(s) and 0 field(s)", + "summary": { + "invalidAgent": 0, + "invalidCronTrigger": 0, + "invalidDatabaseEventTrigger": 0, + "invalidFieldMetadata": 0, + "invalidIndex": 0, + "invalidObjectMetadata": 0, + "invalidPageLayout": 0, + "invalidPageLayoutTab": 0, + "invalidPageLayoutWidget": 0, + "invalidRole": 0, + "invalidRoleTarget": 0, + "invalidRouteTrigger": 0, + "invalidServerlessFunction": 0, + "invalidView": 0, + "invalidViewField": 0, + "invalidViewFilter": 0, + "invalidViewGroup": 0, + "totalErrors": 0, + }, + "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + }, + "message": "Multiple validation errors occurred while creating page layout widget", + "name": "GraphQLError", +} +`; + +exports[`Page layout widget creation should fail when title is missing 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "http": { + "status": 400, + }, + "userFriendlyMessage": "An error occurred.", + }, + "message": "Field "title" of required type "String!" was not provided.", + "name": "GraphQLError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-deletion.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-deletion.integration-spec.ts.snap new file mode 100644 index 00000000000..0eb1eaf3c36 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-deletion.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout widget deletion should fail when deleting a non-existent page layout widget 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Page layout widget to delete not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-update.integration-spec.ts.snap new file mode 100644 index 00000000000..5367dabf0d7 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/failing-page-layout-widget-update.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout widget update should fail when updating a non-existent page layout widget 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Page layout widget with ID "ed3752e3-db7f-42ff-82c0-2757a5410044" not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-creation.integration-spec.ts.snap new file mode 100644 index 00000000000..91d73f263de --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-creation.integration-spec.ts.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout widget creation should succeed should create a page layout widget 1`] = ` +{ + "configuration": null, + "createdAt": Any, + "deletedAt": null, + "gridPosition": { + "column": 0, + "columnSpan": 1, + "row": 0, + "rowSpan": 1, + }, + "id": Any, + "objectMetadataId": null, + "pageLayoutTabId": Any, + "title": "Test Widget", + "type": "VIEW", + "updatedAt": Any, +} +`; + +exports[`Page layout widget creation should succeed should create a page layout widget with specific type 1`] = ` +{ + "configuration": null, + "createdAt": Any, + "deletedAt": null, + "gridPosition": { + "column": 0, + "columnSpan": 2, + "row": 0, + "rowSpan": 2, + }, + "id": Any, + "objectMetadataId": null, + "pageLayoutTabId": Any, + "title": "Graph Widget", + "type": "GRAPH", + "updatedAt": Any, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-deletion.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-deletion.integration-spec.ts.snap new file mode 100644 index 00000000000..f191d4b46ba --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-deletion.integration-spec.ts.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout widget deletion should succeed should soft delete and restore a page layout widget 1`] = ` +{ + "configuration": null, + "createdAt": Any, + "deletedAt": Any, + "gridPosition": { + "column": 0, + "columnSpan": 1, + "row": 0, + "rowSpan": 1, + }, + "id": Any, + "objectMetadataId": null, + "pageLayoutTabId": Any, + "title": "Widget To Delete", + "type": "VIEW", + "updatedAt": Any, +} +`; + +exports[`Page layout widget deletion should succeed should soft delete and restore a page layout widget 2`] = ` +{ + "configuration": null, + "createdAt": Any, + "deletedAt": null, + "gridPosition": { + "column": 0, + "columnSpan": 1, + "row": 0, + "rowSpan": 1, + }, + "id": Any, + "objectMetadataId": null, + "pageLayoutTabId": Any, + "title": "Widget To Delete", + "type": "VIEW", + "updatedAt": Any, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-update.integration-spec.ts.snap new file mode 100644 index 00000000000..c7dd3dc9f77 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/__snapshots__/successful-page-layout-widget-update.integration-spec.ts.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout widget update should succeed should update page layout widget grid position 1`] = ` +{ + "configuration": null, + "createdAt": Any, + "deletedAt": null, + "gridPosition": { + "column": 3, + "columnSpan": 4, + "row": 2, + "rowSpan": 2, + }, + "id": Any, + "objectMetadataId": null, + "pageLayoutTabId": Any, + "title": "Original Widget Title", + "type": "VIEW", + "updatedAt": Any, +} +`; + +exports[`Page layout widget update should succeed should update page layout widget title 1`] = ` +{ + "configuration": null, + "createdAt": Any, + "deletedAt": null, + "gridPosition": { + "column": 0, + "columnSpan": 1, + "row": 0, + "rowSpan": 1, + }, + "id": Any, + "objectMetadataId": null, + "pageLayoutTabId": Any, + "title": "Updated Widget Title", + "type": "VIEW", + "updatedAt": Any, +} +`; + +exports[`Page layout widget update should succeed should update page layout widget type 1`] = ` +{ + "configuration": null, + "createdAt": Any, + "deletedAt": null, + "gridPosition": { + "column": 0, + "columnSpan": 1, + "row": 0, + "rowSpan": 1, + }, + "id": Any, + "objectMetadataId": null, + "pageLayoutTabId": Any, + "title": "Original Widget Title", + "type": "GRAPH", + "updatedAt": Any, +} +`; diff --git a/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant.ts similarity index 66% rename from packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts rename to packages/twenty-server/test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant.ts index c29bd0f0455..d67e9210314 100644 --- a/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant.ts @@ -1,45 +1,29 @@ -export const PAGE_LAYOUT_GQL_FIELDS = ` - id - name - type - objectMetadataId - createdAt - updatedAt - deletedAt -`; - -export const PAGE_LAYOUT_TAB_GQL_FIELDS = ` - id - title - position - pageLayoutId - createdAt - updatedAt - deletedAt -`; - -export const PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS = ` - ... on IframeConfiguration { - url - } +export const WIDGET_CONFIGURATION_GQL_FIELDS = ` ... on BarChartConfiguration { graphType aggregateFieldMetadataId aggregateOperation primaryAxisGroupByFieldMetadataId primaryAxisGroupBySubFieldName + primaryAxisDateGranularity primaryAxisOrderBy secondaryAxisGroupByFieldMetadataId secondaryAxisGroupBySubFieldName + secondaryAxisGroupByDateGranularity secondaryAxisOrderBy omitNullValues axisNameDisplay displayDataLabel + displayLegend rangeMin rangeMax - filter color description + filter + groupMode + isCumulative + timezone + firstDayOfTheWeek } ... on LineChartConfiguration { graphType @@ -47,64 +31,75 @@ export const PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS = ` aggregateOperation primaryAxisGroupByFieldMetadataId primaryAxisGroupBySubFieldName + primaryAxisDateGranularity primaryAxisOrderBy secondaryAxisGroupByFieldMetadataId secondaryAxisGroupBySubFieldName + secondaryAxisGroupByDateGranularity secondaryAxisOrderBy omitNullValues axisNameDisplay displayDataLabel + displayLegend rangeMin rangeMax - filter color description + filter + isStacked + isCumulative + timezone + firstDayOfTheWeek } ... on PieChartConfiguration { graphType groupByFieldMetadataId aggregateFieldMetadataId aggregateOperation + groupBySubFieldName + dateGranularity orderBy displayDataLabel - filter + showCenterMetric + displayLegend color description + filter + timezone + firstDayOfTheWeek } ... on AggregateChartConfiguration { graphType aggregateFieldMetadataId aggregateOperation + label + displayDataLabel + format description filter - format - label + prefix + suffix + timezone + firstDayOfTheWeek } ... on GaugeChartConfiguration { graphType aggregateFieldMetadataId aggregateOperation + displayDataLabel + color description filter + timezone + firstDayOfTheWeek + } + ... on IframeConfiguration { + url + } + ... on StandaloneRichTextConfiguration { + body { + blocknote + markdown + } } `; - -export const PAGE_LAYOUT_WIDGET_GQL_FIELDS = ` - id - title - type - pageLayoutTabId - objectMetadataId - gridPosition { - row - column - rowSpan - columnSpan - } - configuration { - ${PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS} - } - createdAt - updatedAt - deletedAt -`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-creation.integration-spec.ts new file mode 100644 index 00000000000..0f514137c4b --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-creation.integration-spec.ts @@ -0,0 +1,97 @@ +import { faker } from '@faker-js/faker'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util'; +import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util'; +import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util'; +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; +import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; + +import { type CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout-widget.input'; + +describe('Page layout widget creation should fail', () => { + let testPageLayoutId: string; + let testPageLayoutTabId: string; + + beforeAll(async () => { + const { data: layoutData } = await createOnePageLayout({ + expectToFail: false, + input: { name: 'Test Page Layout For Widget Creation Failures' }, + }); + + testPageLayoutId = layoutData.createPageLayout.id; + + const { data: tabData } = await createOnePageLayoutTab({ + expectToFail: false, + input: { + title: 'Test Tab For Widget Creation Failures', + pageLayoutId: testPageLayoutId, + }, + }); + + testPageLayoutTabId = tabData.createPageLayoutTab.id; + }); + + afterAll(async () => { + await destroyOnePageLayoutTab({ + expectToFail: false, + input: { id: testPageLayoutTabId }, + }); + await destroyOnePageLayout({ + expectToFail: false, + input: { id: testPageLayoutId }, + }); + }); + + it('when title is missing', async () => { + const { errors } = await createOnePageLayoutWidget({ + expectToFail: true, + input: { + pageLayoutTabId: testPageLayoutTabId, + gridPosition: { + row: 0, + column: 0, + rowSpan: 1, + columnSpan: 1, + }, + } as CreatePageLayoutWidgetInput, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); + + it('when pageLayoutTabId references non-existent tab', async () => { + const { errors } = await createOnePageLayoutWidget({ + expectToFail: true, + input: { + title: 'Widget With Non-Existent Tab', + pageLayoutTabId: faker.string.uuid(), + gridPosition: { + row: 0, + column: 0, + rowSpan: 1, + columnSpan: 1, + }, + }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); + + it('when gridPosition has invalid values', async () => { + const { errors } = await createOnePageLayoutWidget({ + expectToFail: true, + input: { + title: 'Widget With Invalid Grid Position', + pageLayoutTabId: testPageLayoutTabId, + gridPosition: { + row: -1, + column: 0, + rowSpan: 1, + columnSpan: 1, + }, + }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-deletion.integration-spec.ts new file mode 100644 index 00000000000..08468623c8b --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-deletion.integration-spec.ts @@ -0,0 +1,14 @@ +import { faker } from '@faker-js/faker'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { deleteOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/delete-one-page-layout-widget.util'; + +describe('Page layout widget deletion should fail', () => { + it('when deleting a non-existent page layout widget', async () => { + const { errors } = await deleteOnePageLayoutWidget({ + expectToFail: true, + input: { id: faker.string.uuid() }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-update.integration-spec.ts new file mode 100644 index 00000000000..3e8510def9a --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/failing-page-layout-widget-update.integration-spec.ts @@ -0,0 +1,16 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { updateOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget.util'; + +describe('Page layout widget update should fail', () => { + it('when updating a non-existent page layout widget', async () => { + const { errors } = await updateOnePageLayoutWidget({ + expectToFail: true, + input: { + id: 'ed3752e3-db7f-42ff-82c0-2757a5410044', + title: 'Updated Title', + }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-creation.integration-spec.ts new file mode 100644 index 00000000000..f4622668066 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-creation.integration-spec.ts @@ -0,0 +1,122 @@ +import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util'; +import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util'; +import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util'; +import { destroyOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget.util'; +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; +import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; + +import { WidgetType } from 'src/engine/metadata-modules/page-layout/enums/widget-type.enum'; + +type TestContext = { + input: { + title: string; + type?: WidgetType; + gridPosition: { + row: number; + column: number; + rowSpan: number; + columnSpan: number; + }; + }; +}; + +const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [ + { + title: 'create a page layout widget', + context: { + input: { + title: 'Test Widget', + gridPosition: { + row: 0, + column: 0, + rowSpan: 1, + columnSpan: 1, + }, + }, + }, + }, + { + title: 'create a page layout widget with specific type', + context: { + input: { + title: 'Graph Widget', + type: WidgetType.GRAPH, + gridPosition: { + row: 0, + column: 0, + rowSpan: 2, + columnSpan: 2, + }, + }, + }, + }, +]; + +describe('Page layout widget creation should succeed', () => { + let testPageLayoutId: string; + let testPageLayoutTabId: string; + let createdPageLayoutWidgetId: string; + + beforeAll(async () => { + const { data: layoutData } = await createOnePageLayout({ + expectToFail: false, + input: { name: 'Test Page Layout For Widgets' }, + }); + + testPageLayoutId = layoutData.createPageLayout.id; + + const { data: tabData } = await createOnePageLayoutTab({ + expectToFail: false, + input: { + title: 'Test Tab For Widgets', + pageLayoutId: testPageLayoutId, + }, + }); + + testPageLayoutTabId = tabData.createPageLayoutTab.id; + }); + + afterAll(async () => { + await destroyOnePageLayoutTab({ + expectToFail: false, + input: { id: testPageLayoutTabId }, + }); + await destroyOnePageLayout({ + expectToFail: false, + input: { id: testPageLayoutId }, + }); + }); + + afterEach(async () => { + if (createdPageLayoutWidgetId) { + await destroyOnePageLayoutWidget({ + expectToFail: false, + input: { id: createdPageLayoutWidgetId }, + }); + } + }); + + it.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))( + 'should $title', + async ({ context: { input } }) => { + const { data } = await createOnePageLayoutWidget({ + expectToFail: false, + input: { + ...input, + pageLayoutTabId: testPageLayoutTabId, + }, + }); + + createdPageLayoutWidgetId = data?.createPageLayoutWidget?.id; + + expect(data.createPageLayoutWidget).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ ...data.createPageLayoutWidget }), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-deletion.integration-spec.ts new file mode 100644 index 00000000000..f05b950477d --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-deletion.integration-spec.ts @@ -0,0 +1,127 @@ +import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util'; +import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util'; +import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util'; +import { deleteOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/delete-one-page-layout-widget.util'; +import { destroyOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget.util'; +import { restoreOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/restore-one-page-layout-widget.util'; +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; +import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; + +type TestContext = { + title: string; + operation: 'soft-delete-restore' | 'hard-delete'; +}; + +const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [ + { + title: 'soft delete and restore a page layout widget', + context: { + title: 'Widget To Delete', + operation: 'soft-delete-restore', + }, + }, + { + title: 'hard delete a page layout widget', + context: { + title: 'Widget To Destroy', + operation: 'hard-delete', + }, + }, +]; + +describe('Page layout widget deletion should succeed', () => { + let testPageLayoutId: string; + let testPageLayoutTabId: string; + + beforeAll(async () => { + const { data: layoutData } = await createOnePageLayout({ + expectToFail: false, + input: { name: 'Test Page Layout For Widget Deletions' }, + }); + + testPageLayoutId = layoutData.createPageLayout.id; + + const { data: tabData } = await createOnePageLayoutTab({ + expectToFail: false, + input: { + title: 'Test Tab For Widget Deletions', + pageLayoutId: testPageLayoutId, + }, + }); + + testPageLayoutTabId = tabData.createPageLayoutTab.id; + }); + + afterAll(async () => { + await destroyOnePageLayoutTab({ + expectToFail: false, + input: { id: testPageLayoutTabId }, + }); + await destroyOnePageLayout({ + expectToFail: false, + input: { id: testPageLayoutId }, + }); + }); + + it.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))( + 'should $title', + async ({ context: { title, operation } }) => { + const { data: createData } = await createOnePageLayoutWidget({ + expectToFail: false, + input: { + title, + pageLayoutTabId: testPageLayoutTabId, + gridPosition: { + row: 0, + column: 0, + rowSpan: 1, + columnSpan: 1, + }, + }, + }); + + const widgetId = createData.createPageLayoutWidget.id; + + if (operation === 'soft-delete-restore') { + const { data: deleteData } = await deleteOnePageLayoutWidget({ + expectToFail: false, + input: { id: widgetId }, + }); + + expect(deleteData.deletePageLayoutWidget).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ + ...deleteData.deletePageLayoutWidget, + }), + ); + + const { data: restoreData } = await restoreOnePageLayoutWidget({ + expectToFail: false, + input: { id: widgetId }, + }); + + expect(restoreData.restorePageLayoutWidget).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ + ...restoreData.restorePageLayoutWidget, + }), + ); + + await destroyOnePageLayoutWidget({ + expectToFail: false, + input: { id: widgetId }, + }); + } else { + const { data: destroyData } = await destroyOnePageLayoutWidget({ + expectToFail: false, + input: { id: widgetId }, + }); + + expect(destroyData.destroyPageLayoutWidget).toBe(true); + } + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-update.integration-spec.ts new file mode 100644 index 00000000000..c9aefbdb30e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/successful-page-layout-widget-update.integration-spec.ts @@ -0,0 +1,137 @@ +import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util'; +import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util'; +import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util'; +import { destroyOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget.util'; +import { updateOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget.util'; +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; +import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; + +import { WidgetType } from 'src/engine/metadata-modules/page-layout/enums/widget-type.enum'; + +type TestContext = { + input: { + title?: string; + type?: WidgetType; + gridPosition?: { + row: number; + column: number; + rowSpan: number; + columnSpan: number; + }; + }; +}; + +const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [ + { + title: 'update page layout widget title', + context: { + input: { + title: 'Updated Widget Title', + }, + }, + }, + { + title: 'update page layout widget type', + context: { + input: { + type: WidgetType.GRAPH, + }, + }, + }, + { + title: 'update page layout widget grid position', + context: { + input: { + gridPosition: { + row: 2, + column: 3, + rowSpan: 2, + columnSpan: 4, + }, + }, + }, + }, +]; + +describe('Page layout widget update should succeed', () => { + let testPageLayoutId: string; + let testPageLayoutTabId: string; + let testPageLayoutWidgetId: string; + + beforeAll(async () => { + const { data: layoutData } = await createOnePageLayout({ + expectToFail: false, + input: { name: 'Test Page Layout For Widget Updates' }, + }); + + testPageLayoutId = layoutData.createPageLayout.id; + + const { data: tabData } = await createOnePageLayoutTab({ + expectToFail: false, + input: { + title: 'Test Tab For Widget Updates', + pageLayoutId: testPageLayoutId, + }, + }); + + testPageLayoutTabId = tabData.createPageLayoutTab.id; + }); + + afterAll(async () => { + await destroyOnePageLayoutTab({ + expectToFail: false, + input: { id: testPageLayoutTabId }, + }); + await destroyOnePageLayout({ + expectToFail: false, + input: { id: testPageLayoutId }, + }); + }); + + beforeEach(async () => { + const { data } = await createOnePageLayoutWidget({ + expectToFail: false, + input: { + title: 'Original Widget Title', + pageLayoutTabId: testPageLayoutTabId, + gridPosition: { + row: 0, + column: 0, + rowSpan: 1, + columnSpan: 1, + }, + }, + }); + + testPageLayoutWidgetId = data.createPageLayoutWidget.id; + }); + + afterEach(async () => { + await destroyOnePageLayoutWidget({ + expectToFail: false, + input: { id: testPageLayoutWidgetId }, + }); + }); + + it.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))( + 'should $title', + async ({ context: { input } }) => { + const { data } = await updateOnePageLayoutWidget({ + expectToFail: false, + input: { + id: testPageLayoutWidgetId, + ...input, + }, + }); + + expect(data.updatePageLayoutWidget).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ ...data.updatePageLayoutWidget }), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget-query-factory.util.ts new file mode 100644 index 00000000000..6efd07c98ae --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget-query-factory.util.ts @@ -0,0 +1,42 @@ +import gql from 'graphql-tag'; +import { WIDGET_CONFIGURATION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout-widget.input'; +export type CreateOnePageLayoutWidgetFactoryInput = CreatePageLayoutWidgetInput; + +const DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS = ` + id + title + type + pageLayoutTabId + objectMetadataId + gridPosition { + row + column + rowSpan + columnSpan + } + configuration { + ${WIDGET_CONFIGURATION_GQL_FIELDS} + } + createdAt + updatedAt + deletedAt +`; + +export const createOnePageLayoutWidgetQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation CreatePageLayoutWidget($input: CreatePageLayoutWidgetInput!) { + createPageLayoutWidget(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util.ts new file mode 100644 index 00000000000..2ee98d817c0 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util.ts @@ -0,0 +1,44 @@ +import { + type CreateOnePageLayoutWidgetFactoryInput, + createOnePageLayoutWidgetQueryFactory, +} from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-widget.dto'; + +export const createOnePageLayoutWidget = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + createPageLayoutWidget: PageLayoutWidgetDTO; +}> => { + const graphqlOperation = createOnePageLayoutWidgetQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: + 'Page layout widget creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout widget creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/delete-one-page-layout-widget-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/delete-one-page-layout-widget-query-factory.util.ts new file mode 100644 index 00000000000..191dd69b243 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/delete-one-page-layout-widget-query-factory.util.ts @@ -0,0 +1,43 @@ +import gql from 'graphql-tag'; +import { WIDGET_CONFIGURATION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type DeleteOnePageLayoutWidgetFactoryInput = { + id: string; +}; + +const DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS = ` + id + title + type + pageLayoutTabId + objectMetadataId + gridPosition { + row + column + rowSpan + columnSpan + } + configuration { + ${WIDGET_CONFIGURATION_GQL_FIELDS} + } + createdAt + updatedAt + deletedAt +`; + +export const deleteOnePageLayoutWidgetQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation DeletePageLayoutWidget($id: String!) { + deletePageLayoutWidget(id: $id) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/delete-one-page-layout-widget.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/delete-one-page-layout-widget.util.ts new file mode 100644 index 00000000000..70aab81cddd --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/delete-one-page-layout-widget.util.ts @@ -0,0 +1,44 @@ +import { + type DeleteOnePageLayoutWidgetFactoryInput, + deleteOnePageLayoutWidgetQueryFactory, +} from 'test/integration/metadata/suites/page-layout-widget/utils/delete-one-page-layout-widget-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-widget.dto'; + +export const deleteOnePageLayoutWidget = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + deletePageLayoutWidget: PageLayoutWidgetDTO; +}> => { + const graphqlOperation = deleteOnePageLayoutWidgetQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: + 'Page layout widget deletion should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout widget deletion has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget-query-factory.util.ts new file mode 100644 index 00000000000..ce972027e0d --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget-query-factory.util.ts @@ -0,0 +1,19 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type DestroyOnePageLayoutWidgetFactoryInput = { + id: string; +}; + +export const destroyOnePageLayoutWidgetQueryFactory = ({ + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation DestroyPageLayoutWidget($id: String!) { + destroyPageLayoutWidget(id: $id) + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget.util.ts new file mode 100644 index 00000000000..0549b07b045 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget.util.ts @@ -0,0 +1,39 @@ +import { + type DestroyOnePageLayoutWidgetFactoryInput, + destroyOnePageLayoutWidgetQueryFactory, +} from 'test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +export const destroyOnePageLayoutWidget = async ({ + input, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + destroyPageLayoutWidget: boolean; +}> => { + const graphqlOperation = destroyOnePageLayoutWidgetQueryFactory({ + input, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout widget destroy should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout widget destroy has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/find-one-page-layout-widget-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/find-one-page-layout-widget-query-factory.util.ts new file mode 100644 index 00000000000..1e1bad02aa1 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/find-one-page-layout-widget-query-factory.util.ts @@ -0,0 +1,43 @@ +import gql from 'graphql-tag'; +import { WIDGET_CONFIGURATION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type FindOnePageLayoutWidgetFactoryInput = { + id: string; +}; + +const DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS = ` + id + title + type + pageLayoutTabId + objectMetadataId + gridPosition { + row + column + rowSpan + columnSpan + } + configuration { + ${WIDGET_CONFIGURATION_GQL_FIELDS} + } + createdAt + updatedAt + deletedAt +`; + +export const findOnePageLayoutWidgetQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + query GetPageLayoutWidget($id: String!) { + getPageLayoutWidget(id: $id) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/find-one-page-layout-widget.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/find-one-page-layout-widget.util.ts new file mode 100644 index 00000000000..246185a23e4 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/find-one-page-layout-widget.util.ts @@ -0,0 +1,43 @@ +import { + type FindOnePageLayoutWidgetFactoryInput, + findOnePageLayoutWidgetQueryFactory, +} from 'test/integration/metadata/suites/page-layout-widget/utils/find-one-page-layout-widget-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-widget.dto'; + +export const findOnePageLayoutWidget = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + getPageLayoutWidget: PageLayoutWidgetDTO; +}> => { + const graphqlOperation = findOnePageLayoutWidgetQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Find page layout widget should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Find page layout widget has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/find-page-layout-widgets-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/find-page-layout-widgets-query-factory.util.ts new file mode 100644 index 00000000000..5a4bd084dd4 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/find-page-layout-widgets-query-factory.util.ts @@ -0,0 +1,43 @@ +import gql from 'graphql-tag'; +import { WIDGET_CONFIGURATION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type FindPageLayoutWidgetsFactoryInput = { + pageLayoutTabId: string; +}; + +const DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS = ` + id + title + type + pageLayoutTabId + objectMetadataId + gridPosition { + row + column + rowSpan + columnSpan + } + configuration { + ${WIDGET_CONFIGURATION_GQL_FIELDS} + } + createdAt + updatedAt + deletedAt +`; + +export const findPageLayoutWidgetsQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + query GetPageLayoutWidgets($pageLayoutTabId: String!) { + getPageLayoutWidgets(pageLayoutTabId: $pageLayoutTabId) { + ${gqlFields} + } + } + `, + variables: { + pageLayoutTabId: input.pageLayoutTabId, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/find-page-layout-widgets.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/find-page-layout-widgets.util.ts new file mode 100644 index 00000000000..7cabc2397a8 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/find-page-layout-widgets.util.ts @@ -0,0 +1,43 @@ +import { + type FindPageLayoutWidgetsFactoryInput, + findPageLayoutWidgetsQueryFactory, +} from 'test/integration/metadata/suites/page-layout-widget/utils/find-page-layout-widgets-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-widget.dto'; + +export const findPageLayoutWidgets = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + getPageLayoutWidgets: PageLayoutWidgetDTO[]; +}> => { + const graphqlOperation = findPageLayoutWidgetsQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Find page layout widgets should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Find page layout widgets has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/restore-one-page-layout-widget-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/restore-one-page-layout-widget-query-factory.util.ts new file mode 100644 index 00000000000..cb55cd53796 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/restore-one-page-layout-widget-query-factory.util.ts @@ -0,0 +1,43 @@ +import gql from 'graphql-tag'; +import { WIDGET_CONFIGURATION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type RestoreOnePageLayoutWidgetFactoryInput = { + id: string; +}; + +const DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS = ` + id + title + type + pageLayoutTabId + objectMetadataId + gridPosition { + row + column + rowSpan + columnSpan + } + configuration { + ${WIDGET_CONFIGURATION_GQL_FIELDS} + } + createdAt + updatedAt + deletedAt +`; + +export const restoreOnePageLayoutWidgetQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation RestorePageLayoutWidget($id: String!) { + restorePageLayoutWidget(id: $id) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/restore-one-page-layout-widget.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/restore-one-page-layout-widget.util.ts new file mode 100644 index 00000000000..80402f4fb3f --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/restore-one-page-layout-widget.util.ts @@ -0,0 +1,43 @@ +import { + type RestoreOnePageLayoutWidgetFactoryInput, + restoreOnePageLayoutWidgetQueryFactory, +} from 'test/integration/metadata/suites/page-layout-widget/utils/restore-one-page-layout-widget-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-widget.dto'; + +export const restoreOnePageLayoutWidget = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + restorePageLayoutWidget: PageLayoutWidgetDTO; +}> => { + const graphqlOperation = restoreOnePageLayoutWidgetQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout widget restore should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout widget restore has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget-query-factory.util.ts new file mode 100644 index 00000000000..eccdc7a9183 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget-query-factory.util.ts @@ -0,0 +1,52 @@ +import gql from 'graphql-tag'; +import { WIDGET_CONFIGURATION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-widget.input'; + +export type UpdateOnePageLayoutWidgetFactoryInput = { + id: string; +} & UpdatePageLayoutWidgetInput; + +const DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS = ` + id + title + type + pageLayoutTabId + objectMetadataId + gridPosition { + row + column + rowSpan + columnSpan + } + configuration { + ${WIDGET_CONFIGURATION_GQL_FIELDS} + } + createdAt + updatedAt + deletedAt +`; + +export const updateOnePageLayoutWidgetQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation UpdatePageLayoutWidget($id: String!, $input: UpdatePageLayoutWidgetInput!) { + updatePageLayoutWidget(id: $id, input: $input) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + input: { + title: input.title, + type: input.type, + objectMetadataId: input.objectMetadataId, + gridPosition: input.gridPosition, + configuration: input.configuration, + }, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget.util.ts new file mode 100644 index 00000000000..f652beb1556 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget.util.ts @@ -0,0 +1,43 @@ +import { + type UpdateOnePageLayoutWidgetFactoryInput, + updateOnePageLayoutWidgetQueryFactory, +} from 'test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout-widget.dto'; + +export const updateOnePageLayoutWidget = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + updatePageLayoutWidget: PageLayoutWidgetDTO; +}> => { + const graphqlOperation = updateOnePageLayoutWidgetQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout widget update should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout widget update has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/failing-page-layout-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/failing-page-layout-creation.integration-spec.ts.snap new file mode 100644 index 00000000000..ab6d06a8c30 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/failing-page-layout-creation.integration-spec.ts.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout creation should fail when name is empty string 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", + }, + "message": "name should not be empty", + "name": "UserInputError", +} +`; + +exports[`Page layout creation should fail when name is missing 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "http": { + "status": 400, + }, + "userFriendlyMessage": "An error occurred.", + }, + "message": "Field "name" of required type "String!" was not provided.", + "name": "GraphQLError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/failing-page-layout-deletion.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/failing-page-layout-deletion.integration-spec.ts.snap new file mode 100644 index 00000000000..e38f9fc46ef --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/failing-page-layout-deletion.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout deletion should fail when deleting a non-existent page layout 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Page layout to delete not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/failing-page-layout-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/failing-page-layout-update.integration-spec.ts.snap new file mode 100644 index 00000000000..443b1990b79 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/failing-page-layout-update.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout update should fail when updating a non-existent page layout 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Page layout to update not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/failing-page-layout-with-tabs-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/failing-page-layout-with-tabs-update.integration-spec.ts.snap new file mode 100644 index 00000000000..8dce0bf3ca8 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/failing-page-layout-with-tabs-update.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout with tabs update should fail when updating a non-existent page layout 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Page layout with ID "5b66ba47-198c-4b0d-8583-cd4745f8f9f3" not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-creation.integration-spec.ts.snap new file mode 100644 index 00000000000..861f75c09fc --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-creation.integration-spec.ts.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout creation should succeed should create a page layout with DASHBOARD type 1`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "name": "Dashboard Layout", + "objectMetadataId": null, + "tabs": null, + "type": "DASHBOARD", + "updatedAt": Any, +} +`; + +exports[`Page layout creation should succeed should create a page layout with minimal input 1`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "name": "Test Page Layout", + "objectMetadataId": null, + "tabs": null, + "type": "RECORD_PAGE", + "updatedAt": Any, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-deletion.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-deletion.integration-spec.ts.snap new file mode 100644 index 00000000000..57df910bdf8 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-deletion.integration-spec.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout deletion should succeed should soft delete and restore a page layout 1`] = ` +{ + "createdAt": Any, + "deletedAt": Any, + "id": Any, + "name": "Page Layout To Delete", + "objectMetadataId": null, + "type": "RECORD_PAGE", + "updatedAt": Any, +} +`; + +exports[`Page layout deletion should succeed should soft delete and restore a page layout 2`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "name": "Page Layout To Delete", + "objectMetadataId": null, + "type": "RECORD_PAGE", + "updatedAt": Any, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-update.integration-spec.ts.snap new file mode 100644 index 00000000000..74859f72bb6 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-update.integration-spec.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout update should succeed should update page layout name 1`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "name": "Updated Page Layout Name", + "objectMetadataId": null, + "type": "RECORD_PAGE", + "updatedAt": Any, +} +`; + +exports[`Page layout update should succeed should update page layout type 1`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "name": "Original Page Layout", + "objectMetadataId": null, + "type": "DASHBOARD", + "updatedAt": Any, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-update.integration-spec.ts.snap new file mode 100644 index 00000000000..fa55e8481d8 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-update.integration-spec.ts.snap @@ -0,0 +1,147 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Page layout with tabs update should succeed should update page layout with multiple tabs and different widget types 1`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "name": "Multi-Tab Layout", + "objectMetadataId": null, + "tabs": [ + { + "createdAt": Any, + "deletedAt": null, + "id": Any, + "pageLayoutId": Any, + "position": 0, + "title": "First Tab", + "updatedAt": Any, + "widgets": [ + { + "configuration": { + "aggregateFieldMetadataId": Any, + "aggregateOperation": "COUNT", + "color": null, + "dateGranularity": "DAY", + "description": null, + "displayDataLabel": false, + "displayLegend": true, + "filter": null, + "firstDayOfTheWeek": 1, + "graphType": "PIE", + "groupByFieldMetadataId": Any, + "groupBySubFieldName": null, + "orderBy": "VALUE_DESC", + "showCenterMetric": true, + "timezone": "UTC", + }, + "createdAt": Any, + "deletedAt": null, + "gridPosition": { + "column": 0, + "columnSpan": 1, + "row": 0, + "rowSpan": 1, + }, + "id": Any, + "objectMetadataId": null, + "pageLayoutTabId": Any, + "title": "Pie Chart Widget", + "type": "GRAPH", + "updatedAt": Any, + }, + ], + }, + { + "createdAt": Any, + "deletedAt": null, + "id": Any, + "pageLayoutId": Any, + "position": 1, + "title": "Second Tab", + "updatedAt": Any, + "widgets": [ + { + "configuration": { + "url": "https://example.com", + }, + "createdAt": Any, + "deletedAt": null, + "gridPosition": { + "column": 0, + "columnSpan": 1, + "row": 0, + "rowSpan": 1, + }, + "id": Any, + "objectMetadataId": null, + "pageLayoutTabId": Any, + "title": "Iframe Widget", + "type": "IFRAME", + "updatedAt": Any, + }, + ], + }, + ], + "type": "RECORD_PAGE", + "updatedAt": Any, +} +`; + +exports[`Page layout with tabs update should succeed should update page layout with single tab and widget 1`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "name": "Updated Page Layout", + "objectMetadataId": null, + "tabs": [ + { + "createdAt": Any, + "deletedAt": null, + "id": Any, + "pageLayoutId": Any, + "position": 0, + "title": "Updated Tab 1", + "updatedAt": Any, + "widgets": [ + { + "configuration": { + "aggregateFieldMetadataId": Any, + "aggregateOperation": "COUNT", + "color": null, + "dateGranularity": "DAY", + "description": null, + "displayDataLabel": false, + "displayLegend": true, + "filter": null, + "firstDayOfTheWeek": 1, + "graphType": "PIE", + "groupByFieldMetadataId": Any, + "groupBySubFieldName": null, + "orderBy": "VALUE_DESC", + "showCenterMetric": true, + "timezone": "UTC", + }, + "createdAt": Any, + "deletedAt": null, + "gridPosition": { + "column": 0, + "columnSpan": 1, + "row": 0, + "rowSpan": 1, + }, + "id": Any, + "objectMetadataId": null, + "pageLayoutTabId": Any, + "title": "Pie Chart Widget", + "type": "GRAPH", + "updatedAt": Any, + }, + ], + }, + ], + "type": "RECORD_PAGE", + "updatedAt": Any, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/failing-page-layout-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/failing-page-layout-creation.integration-spec.ts new file mode 100644 index 00000000000..2383f2e15a2 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/failing-page-layout-creation.integration-spec.ts @@ -0,0 +1,24 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; + +import { type CreatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout.input'; + +describe('Page layout creation should fail', () => { + it('when name is missing', async () => { + const { errors } = await createOnePageLayout({ + expectToFail: true, + input: {} as CreatePageLayoutInput, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); + + it('when name is empty string', async () => { + const { errors } = await createOnePageLayout({ + expectToFail: true, + input: { name: '' }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/failing-page-layout-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/failing-page-layout-deletion.integration-spec.ts new file mode 100644 index 00000000000..210ab3d9afe --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/failing-page-layout-deletion.integration-spec.ts @@ -0,0 +1,14 @@ +import { faker } from '@faker-js/faker'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { deleteOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/delete-one-page-layout.util'; + +describe('Page layout deletion should fail', () => { + it('when deleting a non-existent page layout', async () => { + const { errors } = await deleteOnePageLayout({ + expectToFail: true, + input: { id: faker.string.uuid() }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/failing-page-layout-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/failing-page-layout-update.integration-spec.ts new file mode 100644 index 00000000000..d9decb06694 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/failing-page-layout-update.integration-spec.ts @@ -0,0 +1,17 @@ +import { faker } from '@faker-js/faker'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { updateOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/update-one-page-layout.util'; + +describe('Page layout update should fail', () => { + it('when updating a non-existent page layout', async () => { + const { errors } = await updateOnePageLayout({ + expectToFail: true, + input: { + id: faker.string.uuid(), + name: 'Updated Name', + }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/failing-page-layout-with-tabs-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/failing-page-layout-with-tabs-update.integration-spec.ts new file mode 100644 index 00000000000..b9c447a545a --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/failing-page-layout-with-tabs-update.integration-spec.ts @@ -0,0 +1,42 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { updateOnePageLayoutWithTabsAndWidgets } from 'test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets.util'; + +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'; + +describe('Page layout with tabs update should fail', () => { + it('when updating a non-existent page layout', async () => { + const tabId = 'bb4bd7f2-2f89-429c-9d85-8c20a6f776ea'; + const widgetId = 'cf89849b-2106-4b51-8945-359ffeb88141'; + + const { errors } = await updateOnePageLayoutWithTabsAndWidgets({ + expectToFail: true, + input: { + id: '5b66ba47-198c-4b0d-8583-cd4745f8f9f3', + name: 'Updated Name', + type: PageLayoutType.RECORD_PAGE, + objectMetadataId: null, + tabs: [ + { + id: tabId, + title: 'Tab 1', + position: 0, + widgets: [ + { + id: widgetId, + pageLayoutTabId: tabId, + title: 'Widget 1', + type: WidgetType.FIELDS, + objectMetadataId: null, + gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, + configuration: null, + }, + ], + }, + ], + }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-creation.integration-spec.ts new file mode 100644 index 00000000000..a32f1dfc6b3 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-creation.integration-spec.ts @@ -0,0 +1,67 @@ +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; +import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; + +import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; + +type TestContext = { + input: { + name: string; + type?: PageLayoutType; + }; +}; + +const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [ + { + title: 'create a page layout with minimal input', + context: { + input: { + name: 'Test Page Layout', + }, + }, + }, + { + title: 'create a page layout with DASHBOARD type', + context: { + input: { + name: 'Dashboard Layout', + type: PageLayoutType.DASHBOARD, + }, + }, + }, +]; + +describe('Page layout creation should succeed', () => { + let createdPageLayoutId: string; + + afterEach(async () => { + if (createdPageLayoutId) { + await destroyOnePageLayout({ + expectToFail: false, + input: { id: createdPageLayoutId }, + }); + + createdPageLayoutId = ''; + } + }); + + it.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))( + 'should $title', + async ({ context: { input } }) => { + const { data } = await createOnePageLayout({ + expectToFail: false, + input, + }); + + createdPageLayoutId = data?.createPageLayout?.id; + + expect(data.createPageLayout).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ ...data.createPageLayout }), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-deletion.integration-spec.ts new file mode 100644 index 00000000000..af41bc79e08 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-deletion.integration-spec.ts @@ -0,0 +1,81 @@ +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; +import { deleteOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/delete-one-page-layout.util'; +import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; +import { restoreOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/restore-one-page-layout.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; + +type TestContext = { + name: string; + operation: 'soft-delete-restore' | 'hard-delete'; +}; + +const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [ + { + title: 'soft delete and restore a page layout', + context: { + name: 'Page Layout To Delete', + operation: 'soft-delete-restore', + }, + }, + { + title: 'hard delete a page layout', + context: { + name: 'Page Layout To Destroy', + operation: 'hard-delete', + }, + }, +]; + +describe('Page layout deletion should succeed', () => { + it.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))( + 'should $title', + async ({ context: { name, operation } }) => { + const { data: createData } = await createOnePageLayout({ + expectToFail: false, + input: { name }, + }); + + const pageLayoutId = createData.createPageLayout.id; + + if (operation === 'soft-delete-restore') { + const { data: deleteData } = await deleteOnePageLayout({ + expectToFail: false, + input: { id: pageLayoutId }, + }); + + expect(deleteData.deletePageLayout).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ + ...deleteData.deletePageLayout, + }), + ); + + const { data: restoreData } = await restoreOnePageLayout({ + expectToFail: false, + input: { id: pageLayoutId }, + }); + + expect(restoreData.restorePageLayout).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ + ...restoreData.restorePageLayout, + }), + ); + + await destroyOnePageLayout({ + expectToFail: false, + input: { id: pageLayoutId }, + }); + } else { + const { data: destroyData } = await destroyOnePageLayout({ + expectToFail: false, + input: { id: pageLayoutId }, + }); + + expect(destroyData.destroyPageLayout).toBe(true); + } + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-update.integration-spec.ts new file mode 100644 index 00000000000..028b29bbfdc --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-update.integration-spec.ts @@ -0,0 +1,76 @@ +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; +import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; +import { updateOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/update-one-page-layout.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; + +import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; + +type TestContext = { + input: { + name?: string; + type?: PageLayoutType; + }; +}; + +const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [ + { + title: 'update page layout name', + context: { + input: { + name: 'Updated Page Layout Name', + }, + }, + }, + { + title: 'update page layout type', + context: { + input: { + type: PageLayoutType.DASHBOARD, + }, + }, + }, +]; + +describe('Page layout update should succeed', () => { + let testPageLayoutId: string; + + beforeEach(async () => { + const { data } = await createOnePageLayout({ + expectToFail: false, + input: { + name: 'Original Page Layout', + type: PageLayoutType.RECORD_PAGE, + }, + }); + + testPageLayoutId = data.createPageLayout.id; + }); + + afterEach(async () => { + await destroyOnePageLayout({ + expectToFail: false, + input: { id: testPageLayoutId }, + }); + }); + + it.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))( + 'should $title', + async ({ context: { input } }) => { + const { data } = await updateOnePageLayout({ + expectToFail: false, + input: { + id: testPageLayoutId, + ...input, + }, + }); + + expect(data.updatePageLayout).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ ...data.updatePageLayout }), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-with-tabs-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-with-tabs-update.integration-spec.ts new file mode 100644 index 00000000000..03972d84228 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-with-tabs-update.integration-spec.ts @@ -0,0 +1,209 @@ +import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util'; +import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util'; +import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; +import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; +import { updateOnePageLayoutWithTabsAndWidgets } from 'test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; +import { v4 } from 'uuid'; + +import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +import { GraphType } from 'src/engine/metadata-modules/page-layout/enums/graph-type.enum'; +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'; + +const MOCK_PIE_CHART_CONFIGURATION = { + graphType: GraphType.PIE, + aggregateFieldMetadataId: '20202020-77d3-42ff-adc6-cffdad2792c7', + aggregateOperation: AggregateOperations.COUNT, + groupByFieldMetadataId: '20202020-a9fd-4071-9082-db4870ed2430', +}; + +const MOCK_IFRAME_CONFIGURATION = { + url: 'https://example.com', +}; + +type TestContext = { + layoutName: string; + buildTabs: (params: { + tabId1: string; + tabId2: string; + pieChartWidgetId: string; + iframeWidgetId: string; + }) => Array<{ + id: string; + title: string; + position: number; + widgets: Array<{ + id: string; + pageLayoutTabId: string; + title: string; + type: WidgetType; + objectMetadataId: null; + gridPosition: { + row: number; + column: number; + rowSpan: number; + columnSpan: number; + }; + configuration: + | typeof MOCK_PIE_CHART_CONFIGURATION + | typeof MOCK_IFRAME_CONFIGURATION; + }>; + }>; +}; + +const SUCCESSFUL_TEST_CASES: EachTestingContext[] = [ + { + title: 'update page layout with single tab and widget', + context: { + layoutName: 'Updated Page Layout', + buildTabs: ({ tabId1, pieChartWidgetId }) => [ + { + id: tabId1, + title: 'Updated Tab 1', + position: 0, + widgets: [ + { + id: pieChartWidgetId, + pageLayoutTabId: tabId1, + title: 'Pie Chart Widget', + type: WidgetType.GRAPH, + objectMetadataId: null, + gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, + configuration: MOCK_PIE_CHART_CONFIGURATION, + }, + ], + }, + ], + }, + }, + { + title: 'update page layout with multiple tabs and different widget types', + context: { + layoutName: 'Multi-Tab Layout', + buildTabs: ({ tabId1, tabId2, pieChartWidgetId, iframeWidgetId }) => [ + { + id: tabId1, + title: 'First Tab', + position: 0, + widgets: [ + { + id: pieChartWidgetId, + pageLayoutTabId: tabId1, + title: 'Pie Chart Widget', + type: WidgetType.GRAPH, + objectMetadataId: null, + gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, + configuration: MOCK_PIE_CHART_CONFIGURATION, + }, + ], + }, + { + id: tabId2, + title: 'Second Tab', + position: 1, + widgets: [ + { + id: iframeWidgetId, + pageLayoutTabId: tabId2, + title: 'Iframe Widget', + type: WidgetType.IFRAME, + objectMetadataId: null, + gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 }, + configuration: MOCK_IFRAME_CONFIGURATION, + }, + ], + }, + ], + }, + }, +]; + +describe('Page layout with tabs update should succeed', () => { + let testPageLayoutId: string; + let testTabId1: string; + let testTabId2: string; + + beforeEach(async () => { + const { data: layoutData } = await createOnePageLayout({ + expectToFail: false, + input: { + name: 'Original Page Layout', + type: PageLayoutType.RECORD_PAGE, + }, + }); + + testPageLayoutId = layoutData.createPageLayout.id; + + const { data: tabData1 } = await createOnePageLayoutTab({ + expectToFail: false, + input: { + title: 'Tab 1', + pageLayoutId: testPageLayoutId, + }, + }); + + testTabId1 = tabData1.createPageLayoutTab.id; + + const { data: tabData2 } = await createOnePageLayoutTab({ + expectToFail: false, + input: { + title: 'Tab 2', + pageLayoutId: testPageLayoutId, + }, + }); + + testTabId2 = tabData2.createPageLayoutTab.id; + }); + + afterEach(async () => { + await destroyOnePageLayoutTab({ + expectToFail: false, + input: { id: testTabId1 }, + }); + await destroyOnePageLayoutTab({ + expectToFail: false, + input: { id: testTabId2 }, + }); + await destroyOnePageLayout({ + expectToFail: false, + input: { id: testPageLayoutId }, + }); + }); + + it.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))( + 'should $title', + async ({ context: { layoutName, buildTabs } }) => { + const pieChartWidgetId = v4(); + const iframeWidgetId = v4(); + + const tabParams = { + tabId1: testTabId1, + tabId2: testTabId2, + pieChartWidgetId, + iframeWidgetId, + }; + + const { data } = await updateOnePageLayoutWithTabsAndWidgets({ + expectToFail: false, + input: { + id: testPageLayoutId, + name: layoutName, + type: PageLayoutType.RECORD_PAGE, + objectMetadataId: null, + tabs: buildTabs(tabParams), + }, + }); + + expect(data.updatePageLayoutWithTabsAndWidgets).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ + ...data.updatePageLayoutWithTabsAndWidgets, + }), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/create-one-page-layout-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/create-one-page-layout-query-factory.util.ts new file mode 100644 index 00000000000..2ecfb5f9bef --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/create-one-page-layout-query-factory.util.ts @@ -0,0 +1,41 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type CreatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout.input'; + +export type CreateOnePageLayoutFactoryInput = CreatePageLayoutInput; + +const DEFAULT_PAGE_LAYOUT_GQL_FIELDS = ` + id + name + type + objectMetadataId + createdAt + updatedAt + deletedAt + tabs { + id + title + position + pageLayoutId + createdAt + updatedAt + deletedAt + } +`; + +export const createOnePageLayoutQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation CreatePageLayout($input: CreatePageLayoutInput!) { + createPageLayout(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util.ts new file mode 100644 index 00000000000..dcaf6573473 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util.ts @@ -0,0 +1,43 @@ +import { + type CreateOnePageLayoutFactoryInput, + createOnePageLayoutQueryFactory, +} from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto'; + +export const createOnePageLayout = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + createPageLayout: PageLayoutDTO; +}> => { + const graphqlOperation = createOnePageLayoutQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/delete-one-page-layout-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/delete-one-page-layout-query-factory.util.ts new file mode 100644 index 00000000000..8685d4bbbcf --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/delete-one-page-layout-query-factory.util.ts @@ -0,0 +1,32 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type DeleteOnePageLayoutFactoryInput = { + id: string; +}; + +const DEFAULT_PAGE_LAYOUT_GQL_FIELDS = ` + id + name + type + objectMetadataId + createdAt + updatedAt + deletedAt +`; + +export const deleteOnePageLayoutQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation DeletePageLayout($id: String!) { + deletePageLayout(id: $id) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/delete-one-page-layout.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/delete-one-page-layout.util.ts new file mode 100644 index 00000000000..7ad818249c9 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/delete-one-page-layout.util.ts @@ -0,0 +1,43 @@ +import { + type DeleteOnePageLayoutFactoryInput, + deleteOnePageLayoutQueryFactory, +} from 'test/integration/metadata/suites/page-layout/utils/delete-one-page-layout-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto'; + +export const deleteOnePageLayout = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + deletePageLayout: PageLayoutDTO; +}> => { + const graphqlOperation = deleteOnePageLayoutQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout deletion should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout deletion has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout-query-factory.util.ts new file mode 100644 index 00000000000..b474534045f --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout-query-factory.util.ts @@ -0,0 +1,19 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type DestroyOnePageLayoutFactoryInput = { + id: string; +}; + +export const destroyOnePageLayoutQueryFactory = ({ + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation DestroyPageLayout($id: String!) { + destroyPageLayout(id: $id) + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util.ts new file mode 100644 index 00000000000..8c9fb7473aa --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util.ts @@ -0,0 +1,39 @@ +import { + type DestroyOnePageLayoutFactoryInput, + destroyOnePageLayoutQueryFactory, +} from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +export const destroyOnePageLayout = async ({ + input, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + destroyPageLayout: boolean; +}> => { + const graphqlOperation = destroyOnePageLayoutQueryFactory({ + input, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout destroy should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout destroy has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/find-one-page-layout-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/find-one-page-layout-query-factory.util.ts new file mode 100644 index 00000000000..fb64050f7e5 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/find-one-page-layout-query-factory.util.ts @@ -0,0 +1,41 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type FindOnePageLayoutFactoryInput = { + id: string; +}; + +const DEFAULT_PAGE_LAYOUT_GQL_FIELDS = ` + id + name + type + objectMetadataId + createdAt + updatedAt + deletedAt + tabs { + id + title + position + pageLayoutId + createdAt + updatedAt + deletedAt + } +`; + +export const findOnePageLayoutQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + query GetPageLayout($id: String!) { + getPageLayout(id: $id) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/find-one-page-layout.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/find-one-page-layout.util.ts new file mode 100644 index 00000000000..69d437a72ec --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/find-one-page-layout.util.ts @@ -0,0 +1,43 @@ +import { + type FindOnePageLayoutFactoryInput, + findOnePageLayoutQueryFactory, +} from 'test/integration/metadata/suites/page-layout/utils/find-one-page-layout-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto'; + +export const findOnePageLayout = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + getPageLayout: PageLayoutDTO | null; +}> => { + const graphqlOperation = findOnePageLayoutQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Find page layout should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Find page layout has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/find-page-layouts-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/find-page-layouts-query-factory.util.ts new file mode 100644 index 00000000000..8bdff0fe6e9 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/find-page-layouts-query-factory.util.ts @@ -0,0 +1,32 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type FindPageLayoutsFactoryInput = { + objectMetadataId?: string; +} | void; + +const DEFAULT_PAGE_LAYOUT_GQL_FIELDS = ` + id + name + type + objectMetadataId + createdAt + updatedAt + deletedAt +`; + +export const findPageLayoutsQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + query GetPageLayouts($objectMetadataId: String) { + getPageLayouts(objectMetadataId: $objectMetadataId) { + ${gqlFields} + } + } + `, + variables: { + objectMetadataId: input?.objectMetadataId, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/find-page-layouts.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/find-page-layouts.util.ts new file mode 100644 index 00000000000..af22a49b4a0 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/find-page-layouts.util.ts @@ -0,0 +1,43 @@ +import { + type FindPageLayoutsFactoryInput, + findPageLayoutsQueryFactory, +} from 'test/integration/metadata/suites/page-layout/utils/find-page-layouts-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto'; + +export const findPageLayouts = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + getPageLayouts: PageLayoutDTO[]; +}> => { + const graphqlOperation = findPageLayoutsQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Find page layouts should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Find page layouts has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/restore-one-page-layout-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/restore-one-page-layout-query-factory.util.ts new file mode 100644 index 00000000000..97e989e1fed --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/restore-one-page-layout-query-factory.util.ts @@ -0,0 +1,32 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +export type RestoreOnePageLayoutFactoryInput = { + id: string; +}; + +const DEFAULT_PAGE_LAYOUT_GQL_FIELDS = ` + id + name + type + objectMetadataId + createdAt + updatedAt + deletedAt +`; + +export const restoreOnePageLayoutQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation RestorePageLayout($id: String!) { + restorePageLayout(id: $id) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/restore-one-page-layout.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/restore-one-page-layout.util.ts new file mode 100644 index 00000000000..c961e8c78fe --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/restore-one-page-layout.util.ts @@ -0,0 +1,43 @@ +import { + type RestoreOnePageLayoutFactoryInput, + restoreOnePageLayoutQueryFactory, +} from 'test/integration/metadata/suites/page-layout/utils/restore-one-page-layout-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto'; + +export const restoreOnePageLayout = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + restorePageLayout: PageLayoutDTO; +}> => { + const graphqlOperation = restoreOnePageLayoutQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout restore should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout restore has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-query-factory.util.ts new file mode 100644 index 00000000000..ebebffee075 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-query-factory.util.ts @@ -0,0 +1,39 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type UpdatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout.input'; + +export type UpdateOnePageLayoutFactoryInput = { + id: string; +} & UpdatePageLayoutInput; + +const DEFAULT_PAGE_LAYOUT_GQL_FIELDS = ` + id + name + type + objectMetadataId + createdAt + updatedAt + deletedAt +`; + +export const updateOnePageLayoutQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation UpdatePageLayout($id: String!, $input: UpdatePageLayoutInput!) { + updatePageLayout(id: $id, input: $input) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + input: { + name: input.name, + type: input.type, + objectMetadataId: input.objectMetadataId, + }, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets-query-factory.util.ts new file mode 100644 index 00000000000..ec2d8b636a4 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets-query-factory.util.ts @@ -0,0 +1,69 @@ +import gql from 'graphql-tag'; +import { WIDGET_CONFIGURATION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type UpdatePageLayoutWithTabsInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input'; + +export type UpdateOnePageLayoutWithTabsAndWidgetsFactoryInput = { + id: string; +} & UpdatePageLayoutWithTabsInput; + +const DEFAULT_PAGE_LAYOUT_GQL_FIELDS = ` + id + name + type + objectMetadataId + createdAt + updatedAt + deletedAt + tabs { + id + title + position + pageLayoutId + createdAt + updatedAt + deletedAt + widgets { + id + title + type + pageLayoutTabId + objectMetadataId + gridPosition { + row + column + rowSpan + columnSpan + } + configuration { + ${WIDGET_CONFIGURATION_GQL_FIELDS} + } + createdAt + updatedAt + deletedAt + } + } +`; + +export const updateOnePageLayoutWithTabsAndWidgetsQueryFactory = ({ + input, + gqlFields = DEFAULT_PAGE_LAYOUT_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation UpdatePageLayoutWithTabsAndWidgets($id: String!, $input: UpdatePageLayoutWithTabsInput!) { + updatePageLayoutWithTabsAndWidgets(id: $id, input: $input) { + ${gqlFields} + } + } + `, + variables: { + id: input.id, + input: { + name: input.name, + type: input.type, + objectMetadataId: input.objectMetadataId, + tabs: input.tabs, + }, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets.util.ts new file mode 100644 index 00000000000..a20f40f8725 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets.util.ts @@ -0,0 +1,45 @@ +import { + type UpdateOnePageLayoutWithTabsAndWidgetsFactoryInput, + updateOnePageLayoutWithTabsAndWidgetsQueryFactory, +} from 'test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto'; + +export const updateOnePageLayoutWithTabsAndWidgets = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + updatePageLayoutWithTabsAndWidgets: PageLayoutDTO; +}> => { + const graphqlOperation = updateOnePageLayoutWithTabsAndWidgetsQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: + 'Page layout with tabs and widgets update should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: + 'Page layout with tabs and widgets update has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout.util.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout.util.ts new file mode 100644 index 00000000000..033a6c9543e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/utils/update-one-page-layout.util.ts @@ -0,0 +1,43 @@ +import { + type UpdateOnePageLayoutFactoryInput, + updateOnePageLayoutQueryFactory, +} from 'test/integration/metadata/suites/page-layout/utils/update-one-page-layout-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto'; + +export const updateOnePageLayout = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + updatePageLayout: PageLayoutDTO; +}> => { + const graphqlOperation = updateOnePageLayoutQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Page layout update should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Page layout update has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/rest/suites/page-layout-tab.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/page-layout-tab.integration-spec.ts deleted file mode 100644 index f406668b0fe..00000000000 --- a/packages/twenty-server/test/integration/rest/suites/page-layout-tab.integration-spec.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { TEST_NOT_EXISTING_PAGE_LAYOUT_ID } from 'test/integration/constants/test-page-layout-ids.constants'; -import { TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID } from 'test/integration/constants/test-page-layout-tab-ids.constants'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; -import { - createTestPageLayoutWithRestApi, - deleteTestPageLayoutWithRestApi, -} from 'test/integration/rest/utils/page-layout-rest-api.util'; -import { - createTestPageLayoutTabWithRestApi, - deleteTestPageLayoutTabWithRestApi, -} from 'test/integration/rest/utils/page-layout-tab-rest-api.util'; -import { - assertRestApiErrorResponse, - assertRestApiSuccessfulResponse, -} from 'test/integration/rest/utils/rest-test-assertions.util'; -import { generateRecordName } from 'test/integration/utils/generate-record-name'; -import { - assertPageLayoutTabStructure, - cleanupPageLayoutTabRecords, -} from 'test/integration/utils/page-layout-tab-test.util'; - -import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; -import { - PageLayoutTabExceptionMessageKey, - generatePageLayoutTabExceptionMessage, -} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-tab.exception'; - -describe('Page Layout Tab REST API', () => { - let testObjectMetadataId: string; - let testPageLayoutId: string; - - beforeAll(async () => { - const { - data: { - createOneObject: { id: objectMetadataId }, - }, - } = await createOneObjectMetadata({ - input: { - nameSingular: 'myTestPageLayoutTabObject', - namePlural: 'myTestPageLayoutTabObjects', - labelSingular: 'My Test Page Layout Tab Object', - labelPlural: 'My Test Page Layout Tab Objects', - icon: 'IconLayout', - }, - }); - - testObjectMetadataId = objectMetadataId; - - const testPageLayout = await createTestPageLayoutWithRestApi({ - name: generateRecordName('Test Page Layout for Tabs'), - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - testPageLayoutId = testPageLayout.id; - }); - - afterAll(async () => { - await deleteTestPageLayoutWithRestApi(testPageLayoutId); - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: testObjectMetadataId, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - input: { idToDelete: testObjectMetadataId }, - }); - }); - - afterEach(async () => { - await cleanupPageLayoutTabRecords(); - }); - - describe('GET /rest/metadata/pageLayoutTabs', () => { - it('should return page layout tabs filtered by pageLayoutId', async () => { - const input1 = { - title: 'Tab 1', - pageLayoutId: testPageLayoutId, - position: 0, - }; - const input2 = { - title: 'Tab 2', - pageLayoutId: testPageLayoutId, - position: 1, - }; - - await createTestPageLayoutTabWithRestApi(input1); - await createTestPageLayoutTabWithRestApi(input2); - - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayoutTabs?pageLayoutId=${testPageLayoutId}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - expect(Array.isArray(response.body)).toBe(true); - - if (response.body.length > 0) { - assertPageLayoutTabStructure(response.body[0], input1); - } - }); - - it('should return empty array when no page layout tabs match pageLayoutId', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayoutTabs?pageLayoutId=${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - expect(Array.isArray(response.body)).toBe(true); - expect(response.body).toHaveLength(0); - }); - - it('should return error when pageLayoutId is missing', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: '/metadata/pageLayoutTabs', - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 400, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_ID_REQUIRED, - ), - ); - }); - }); - - describe('POST /rest/metadata/pageLayoutTabs', () => { - it('should create a new page layout tab with all properties', async () => { - const input = { - title: 'Test Tab', - pageLayoutId: testPageLayoutId, - position: 1, - }; - - const pageLayoutTab = await createTestPageLayoutTabWithRestApi(input); - - assertPageLayoutTabStructure(pageLayoutTab, input); - - await deleteTestPageLayoutTabWithRestApi(pageLayoutTab.id); - }); - - it('should create a page layout tab with minimum required fields', async () => { - const input = { - title: 'Minimal Tab', - pageLayoutId: testPageLayoutId, - }; - - const pageLayoutTab = await createTestPageLayoutTabWithRestApi({ - title: input.title, - pageLayoutId: input.pageLayoutId, - }); - - assertPageLayoutTabStructure(pageLayoutTab, { - ...input, - position: 0, - }); - - await deleteTestPageLayoutTabWithRestApi(pageLayoutTab.id); - }); - - it('should return error when creating tab with invalid pageLayoutId', async () => { - const pageLayoutTabData = { - title: 'Invalid Tab', - pageLayoutId: TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - position: 0, - }; - - const response = await makeRestAPIRequest({ - method: 'post', - path: '/metadata/pageLayoutTabs', - body: pageLayoutTabData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 400, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND, - ), - ); - }); - - it('should return error when creating tab without title', async () => { - const pageLayoutTabData = { - pageLayoutId: testPageLayoutId, - position: 0, - }; - - const response = await makeRestAPIRequest({ - method: 'post', - path: '/metadata/pageLayoutTabs', - body: pageLayoutTabData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse(response, 400); - }); - }); - - describe('GET /rest/metadata/pageLayoutTabs/:id', () => { - it('should return a page layout tab by id', async () => { - const input = { - title: 'Tab', - pageLayoutId: testPageLayoutId, - position: 2, - }; - const pageLayoutTab = await createTestPageLayoutTabWithRestApi(input); - - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - assertPageLayoutTabStructure(response.body, { - id: pageLayoutTab.id, - ...input, - }); - - await deleteTestPageLayoutTabWithRestApi(pageLayoutTab.id); - }); - - it('should return error for non-existent page layout tab', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayoutTabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 404, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - ), - ); - }); - }); - - describe('PATCH /rest/metadata/pageLayoutTabs/:id', () => { - it('should update an existing page layout tab', async () => { - const input = { - title: 'Test Tab for Update', - pageLayoutId: testPageLayoutId, - position: 0, - }; - const pageLayoutTab = await createTestPageLayoutTabWithRestApi(input); - - const updateData = { - title: 'Updated Tab', - position: 3, - }; - - const response = await makeRestAPIRequest({ - method: 'patch', - path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`, - body: updateData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - assertPageLayoutTabStructure(response.body, { - id: pageLayoutTab.id, - ...input, - ...updateData, - }); - - await deleteTestPageLayoutTabWithRestApi(pageLayoutTab.id); - }); - - it('should update only provided fields', async () => { - const input = { - title: 'Original Tab', - pageLayoutId: testPageLayoutId, - position: 1, - }; - const pageLayoutTab = await createTestPageLayoutTabWithRestApi(input); - - const updatedTitle = 'Updated Title Only'; - const updateData = { - title: updatedTitle, - }; - - const response = await makeRestAPIRequest({ - method: 'patch', - path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`, - body: updateData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - assertPageLayoutTabStructure(response.body, { - id: pageLayoutTab.id, - title: updatedTitle, - pageLayoutId: testPageLayoutId, - position: 1, - }); - - await deleteTestPageLayoutTabWithRestApi(pageLayoutTab.id); - }); - - it('should return error when updating non-existent page layout tab', async () => { - const updateData = { - title: 'Updated Tab', - position: 5, - }; - - const response = await makeRestAPIRequest({ - method: 'patch', - path: `/metadata/pageLayoutTabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`, - body: updateData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 404, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - ), - ); - }); - }); - - describe('DELETE /rest/metadata/pageLayoutTabs/:id', () => { - it('should delete an existing page layout tab', async () => { - const pageLayoutTabTitle = generateRecordName('Test Tab for Delete'); - const pageLayoutTab = await createTestPageLayoutTabWithRestApi({ - title: pageLayoutTabTitle, - pageLayoutId: testPageLayoutId, - position: 0, - }); - - const deleteResponse = await makeRestAPIRequest({ - method: 'delete', - path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(deleteResponse); - assertPageLayoutTabStructure(deleteResponse.body, pageLayoutTab); - - const getResponse = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - getResponse, - 404, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, - pageLayoutTab.id, - ), - ); - }); - - it('should return error when deleting non-existent page layout tab', async () => { - const response = await makeRestAPIRequest({ - method: 'delete', - path: `/metadata/pageLayoutTabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 404, - generatePageLayoutTabExceptionMessage( - PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - ), - ); - }); - }); -}); diff --git a/packages/twenty-server/test/integration/rest/suites/page-layout-widget.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/page-layout-widget.integration-spec.ts deleted file mode 100644 index ec98580b7d9..00000000000 --- a/packages/twenty-server/test/integration/rest/suites/page-layout-widget.integration-spec.ts +++ /dev/null @@ -1,503 +0,0 @@ -import { TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID } from 'test/integration/constants/test-page-layout-tab-ids.constants'; -import { TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID } from 'test/integration/constants/test-page-layout-widget-ids.constants'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; -import { - createTestPageLayoutWithRestApi, - deleteTestPageLayoutWithRestApi, -} from 'test/integration/rest/utils/page-layout-rest-api.util'; -import { - createTestPageLayoutTabWithRestApi, - deleteTestPageLayoutTabWithRestApi, -} from 'test/integration/rest/utils/page-layout-tab-rest-api.util'; -import { - createTestPageLayoutWidgetWithRestApi, - deleteTestPageLayoutWidgetWithRestApi, -} from 'test/integration/rest/utils/page-layout-widget-rest-api.util'; -import { - assertRestApiErrorResponse, - assertRestApiSuccessfulResponse, -} from 'test/integration/rest/utils/rest-test-assertions.util'; -import { generateRecordName } from 'test/integration/utils/generate-record-name'; -import { - assertPageLayoutWidgetStructure, - cleanupPageLayoutWidgetRecords, -} from 'test/integration/utils/page-layout-widget-test.util'; - -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 { - PageLayoutWidgetExceptionMessageKey, - generatePageLayoutWidgetExceptionMessage, -} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout-widget.exception'; - -describe('Page Layout Widget REST API', () => { - let testObjectMetadataId: string; - let testPageLayoutId: string; - let testPageLayoutTabId: string; - - beforeAll(async () => { - const { - data: { - createOneObject: { id: objectMetadataId }, - }, - } = await createOneObjectMetadata({ - input: { - nameSingular: 'myTestPageLayoutWidgetObject', - namePlural: 'myTestPageLayoutWidgetObjects', - labelSingular: 'My Test Page Layout Widget Object', - labelPlural: 'My Test Page Layout Widget Objects', - icon: 'IconLayout', - }, - }); - - testObjectMetadataId = objectMetadataId; - - const testPageLayout = await createTestPageLayoutWithRestApi({ - name: generateRecordName('Test Page Layout for Widgets'), - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - testPageLayoutId = testPageLayout.id; - - const testPageLayoutTab = await createTestPageLayoutTabWithRestApi({ - title: generateRecordName('Test Page Layout Tab for Widgets'), - pageLayoutId: testPageLayoutId, - position: 0, - }); - - testPageLayoutTabId = testPageLayoutTab.id; - }); - - afterAll(async () => { - await deleteTestPageLayoutTabWithRestApi(testPageLayoutTabId); - await deleteTestPageLayoutWithRestApi(testPageLayoutId); - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: testObjectMetadataId, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - input: { idToDelete: testObjectMetadataId }, - }); - }); - - afterEach(async () => { - await cleanupPageLayoutWidgetRecords(); - }); - - describe('GET /rest/metadata/pageLayoutWidgets', () => { - it('should return page layout widgets filtered by pageLayoutTabId', async () => { - const input1 = { - title: 'Widget 1', - pageLayoutTabId: testPageLayoutTabId, - type: WidgetType.VIEW, - gridPosition: { - row: 0, - column: 0, - rowSpan: 1, - columnSpan: 1, - }, - }; - const input2 = { - title: 'Widget 2', - pageLayoutTabId: testPageLayoutTabId, - type: WidgetType.IFRAME, - gridPosition: { - row: 0, - column: 1, - rowSpan: 1, - columnSpan: 1, - }, - }; - - await createTestPageLayoutWidgetWithRestApi(input1); - await createTestPageLayoutWidgetWithRestApi(input2); - - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayoutWidgets?pageLayoutTabId=${testPageLayoutTabId}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - expect(Array.isArray(response.body)).toBe(true); - expect(response.body.length).toBeGreaterThanOrEqual(2); - - if (response.body.length > 0) { - assertPageLayoutWidgetStructure(response.body[0]); - expect(response.body[0].pageLayoutTabId).toBe(testPageLayoutTabId); - } - }); - - it('should return empty array when no page layout widgets match pageLayoutTabId', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayoutWidgets?pageLayoutTabId=${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - expect(Array.isArray(response.body)).toBe(true); - expect(response.body).toHaveLength(0); - }); - - it('should return error when pageLayoutTabId is missing', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: '/metadata/pageLayoutWidgets', - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 400, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_ID_REQUIRED, - ), - ); - }); - }); - - describe('POST /rest/metadata/pageLayoutWidgets', () => { - it('should create a new page layout widget with all properties', async () => { - const input = { - title: 'Test Widget', - pageLayoutTabId: testPageLayoutTabId, - type: WidgetType.GRAPH, - objectMetadataId: testObjectMetadataId, - gridPosition: { - row: 1, - column: 1, - rowSpan: 2, - columnSpan: 2, - }, - configuration: null, - }; - - const pageLayoutWidget = - await createTestPageLayoutWidgetWithRestApi(input); - - assertPageLayoutWidgetStructure(pageLayoutWidget, input); - - await deleteTestPageLayoutWidgetWithRestApi(pageLayoutWidget.id); - }); - - it('should create a page layout widget with minimum required fields', async () => { - const input = { - title: 'Minimal Widget', - pageLayoutTabId: testPageLayoutTabId, - gridPosition: { - row: 0, - column: 0, - rowSpan: 1, - columnSpan: 1, - }, - }; - - const pageLayoutWidget = await createTestPageLayoutWidgetWithRestApi({ - title: input.title, - pageLayoutTabId: input.pageLayoutTabId, - gridPosition: input.gridPosition, - }); - - assertPageLayoutWidgetStructure(pageLayoutWidget, { - ...input, - type: WidgetType.VIEW, - objectMetadataId: null, - configuration: null, - }); - - await deleteTestPageLayoutWidgetWithRestApi(pageLayoutWidget.id); - }); - - it('should return error when creating widget with invalid pageLayoutTabId', async () => { - const pageLayoutWidgetData = { - title: 'Invalid Widget', - pageLayoutTabId: TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID, - gridPosition: { - row: 0, - column: 0, - rowSpan: 1, - columnSpan: 1, - }, - }; - - const response = await makeRestAPIRequest({ - method: 'post', - path: '/metadata/pageLayoutWidgets', - body: pageLayoutWidgetData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 400, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND, - ), - ); - }); - - it('should return error when creating widget without title', async () => { - const pageLayoutWidgetData = { - pageLayoutTabId: testPageLayoutTabId, - gridPosition: { - row: 0, - column: 0, - rowSpan: 1, - columnSpan: 1, - }, - }; - - const response = await makeRestAPIRequest({ - method: 'post', - path: '/metadata/pageLayoutWidgets', - body: pageLayoutWidgetData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse(response, 400); - }); - - it('should return error when creating widget without gridPosition', async () => { - const pageLayoutWidgetData = { - title: 'Widget Without Grid Position', - pageLayoutTabId: testPageLayoutTabId, - }; - - const response = await makeRestAPIRequest({ - method: 'post', - path: '/metadata/pageLayoutWidgets', - body: pageLayoutWidgetData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse(response, 400); - }); - }); - - describe('GET /rest/metadata/pageLayoutWidgets/:id', () => { - it('should return a page layout widget by id', async () => { - const input = { - title: 'Widget For Get', - pageLayoutTabId: testPageLayoutTabId, - type: WidgetType.FIELDS, - gridPosition: { - row: 2, - column: 2, - rowSpan: 1, - columnSpan: 3, - }, - }; - const pageLayoutWidget = - await createTestPageLayoutWidgetWithRestApi(input); - - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - assertPageLayoutWidgetStructure(response.body, { - id: pageLayoutWidget.id, - ...input, - }); - - await deleteTestPageLayoutWidgetWithRestApi(pageLayoutWidget.id); - }); - - it('should return error for non-existent page layout widget', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayoutWidgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 404, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - ), - ); - }); - }); - - describe('PATCH /rest/metadata/pageLayoutWidgets/:id', () => { - it('should update an existing page layout widget', async () => { - const input = { - title: 'Test Widget for Update', - pageLayoutTabId: testPageLayoutTabId, - type: WidgetType.VIEW, - gridPosition: { - row: 0, - column: 0, - rowSpan: 1, - columnSpan: 1, - }, - }; - const pageLayoutWidget = - await createTestPageLayoutWidgetWithRestApi(input); - - const updateData = { - title: 'Updated Widget', - type: WidgetType.GRAPH, - gridPosition: { - row: 1, - column: 1, - rowSpan: 2, - columnSpan: 3, - }, - configuration: null, - }; - - const response = await makeRestAPIRequest({ - method: 'patch', - path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`, - body: updateData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - assertPageLayoutWidgetStructure(response.body, { - id: pageLayoutWidget.id, - pageLayoutTabId: testPageLayoutTabId, - ...updateData, - }); - - await deleteTestPageLayoutWidgetWithRestApi(pageLayoutWidget.id); - }); - - it('should update only provided fields', async () => { - const input = { - title: 'Original Widget', - pageLayoutTabId: testPageLayoutTabId, - type: WidgetType.IFRAME, - gridPosition: { - row: 1, - column: 1, - rowSpan: 1, - columnSpan: 1, - }, - }; - const pageLayoutWidget = - await createTestPageLayoutWidgetWithRestApi(input); - - const updatedTitle = 'Updated Title Only'; - const updateData = { - title: updatedTitle, - }; - - const response = await makeRestAPIRequest({ - method: 'patch', - path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`, - body: updateData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - assertPageLayoutWidgetStructure(response.body, { - id: pageLayoutWidget.id, - title: updatedTitle, - pageLayoutTabId: testPageLayoutTabId, - type: WidgetType.IFRAME, - gridPosition: input.gridPosition, - }); - - await deleteTestPageLayoutWidgetWithRestApi(pageLayoutWidget.id); - }); - - it('should return error when updating non-existent page layout widget', async () => { - const updateData = { - title: 'Updated Widget', - type: WidgetType.GRAPH, - }; - - const response = await makeRestAPIRequest({ - method: 'patch', - path: `/metadata/pageLayoutWidgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`, - body: updateData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 404, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - ), - ); - }); - }); - - describe('DELETE /rest/metadata/pageLayoutWidgets/:id', () => { - it('should delete an existing page layout widget', async () => { - const pageLayoutWidgetTitle = generateRecordName( - 'Test Widget for Delete', - ); - const pageLayoutWidget = await createTestPageLayoutWidgetWithRestApi({ - title: pageLayoutWidgetTitle, - pageLayoutTabId: testPageLayoutTabId, - type: WidgetType.FIELDS, - gridPosition: { - row: 0, - column: 0, - rowSpan: 1, - columnSpan: 1, - }, - }); - - const deleteResponse = await makeRestAPIRequest({ - method: 'delete', - path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(deleteResponse); - assertPageLayoutWidgetStructure(deleteResponse.body, pageLayoutWidget); - - const getResponse = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - getResponse, - 404, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, - pageLayoutWidget.id, - ), - ); - }); - - it('should return error when deleting non-existent page layout widget', async () => { - const response = await makeRestAPIRequest({ - method: 'delete', - path: `/metadata/pageLayoutWidgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 404, - generatePageLayoutWidgetExceptionMessage( - PageLayoutWidgetExceptionMessageKey.PAGE_LAYOUT_WIDGET_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID, - ), - ); - }); - }); -}); diff --git a/packages/twenty-server/test/integration/rest/suites/page-layout.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/page-layout.integration-spec.ts deleted file mode 100644 index c041a3c7557..00000000000 --- a/packages/twenty-server/test/integration/rest/suites/page-layout.integration-spec.ts +++ /dev/null @@ -1,379 +0,0 @@ -import { TEST_NOT_EXISTING_PAGE_LAYOUT_ID } from 'test/integration/constants/test-page-layout-ids.constants'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; -import { - createTestPageLayoutWithRestApi, - deleteTestPageLayoutWithRestApi, -} from 'test/integration/rest/utils/page-layout-rest-api.util'; -import { - assertRestApiErrorResponse, - assertRestApiSuccessfulResponse, -} from 'test/integration/rest/utils/rest-test-assertions.util'; -import { generateRecordName } from 'test/integration/utils/generate-record-name'; -import { - assertPageLayoutStructure, - cleanupPageLayoutRecords, -} from 'test/integration/utils/page-layout-test.util'; - -import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; -import { - PageLayoutExceptionMessageKey, - generatePageLayoutExceptionMessage, -} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception'; - -describe('Page Layout REST API', () => { - let testObjectMetadataId: string; - - beforeAll(async () => { - const { - data: { - createOneObject: { id: objectMetadataId }, - }, - } = await createOneObjectMetadata({ - input: { - nameSingular: 'myTestPageLayoutObject', - namePlural: 'myTestPageLayoutObjects', - labelSingular: 'My Test Page Layout Object', - labelPlural: 'My Test Page Layout Objects', - icon: 'IconLayout', - }, - }); - - testObjectMetadataId = objectMetadataId; - }); - - afterAll(async () => { - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: testObjectMetadataId, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - input: { idToDelete: testObjectMetadataId }, - }); - }); - - afterEach(async () => { - await cleanupPageLayoutRecords(); - }); - - describe('GET /rest/metadata/pageLayouts', () => { - it('should return all page layouts for workspace', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: '/metadata/pageLayouts', - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - expect(Array.isArray(response.body)).toBe(true); - }); - - it('should return page layouts filtered by objectMetadataId', async () => { - await createTestPageLayoutWithRestApi({ - name: 'Test Page Layout for Filter', - objectMetadataId: testObjectMetadataId, - type: PageLayoutType.RECORD_PAGE, - }); - - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayouts?objectMetadataId=${testObjectMetadataId}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - expect(Array.isArray(response.body)).toBe(true); - - if (response.body.length > 0) { - assertPageLayoutStructure(response.body[0]); - expect(response.body[0].objectMetadataId).toBe(testObjectMetadataId); - } - }); - - it('should return empty array when no page layouts match objectMetadataId', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayouts?objectMetadataId=${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - expect(Array.isArray(response.body)).toBe(true); - expect(response.body).toHaveLength(0); - }); - }); - - describe('POST /rest/metadata/pageLayouts', () => { - it('should create a new page layout with all properties', async () => { - const pageLayoutName = generateRecordName('Dashboard Page Layout'); - const pageLayout = await createTestPageLayoutWithRestApi({ - name: pageLayoutName, - type: PageLayoutType.DASHBOARD, - objectMetadataId: testObjectMetadataId, - }); - - assertPageLayoutStructure(pageLayout, { - name: pageLayoutName, - type: PageLayoutType.DASHBOARD, - objectMetadataId: testObjectMetadataId, - }); - - await deleteTestPageLayoutWithRestApi(pageLayout.id); - }); - - it('should create a page layout with minimum required fields', async () => { - const pageLayoutName = generateRecordName('Minimal Page Layout'); - const pageLayout = await createTestPageLayoutWithRestApi({ - name: pageLayoutName, - }); - - assertPageLayoutStructure(pageLayout, { - name: pageLayoutName, - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: null, - }); - - await deleteTestPageLayoutWithRestApi(pageLayout.id); - }); - - describe('GET /rest/metadata/pageLayouts/:id', () => { - it('should return a page layout by id', async () => { - const pageLayoutName = generateRecordName('Test Page Layout for Get'); - const pageLayout = await createTestPageLayoutWithRestApi({ - name: pageLayoutName, - type: PageLayoutType.DASHBOARD, - objectMetadataId: testObjectMetadataId, - }); - - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayouts/${pageLayout.id}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - assertPageLayoutStructure(response.body, { - id: pageLayout.id, - name: pageLayoutName, - type: PageLayoutType.DASHBOARD, - objectMetadataId: testObjectMetadataId, - }); - - await deleteTestPageLayoutWithRestApi(pageLayout.id); - }); - - it('should return {} for non-existent page layout', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 404, - generatePageLayoutExceptionMessage( - PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - ), - ); - }); - }); - - describe('PATCH /rest/metadata/pageLayouts/:id', () => { - it('should update an existing page layout', async () => { - const pageLayoutName = generateRecordName( - 'Test Page Layout for Update', - ); - const pageLayout = await createTestPageLayoutWithRestApi({ - name: pageLayoutName, - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - const updatedName = generateRecordName('Updated Page Layout'); - const updateData = { - name: updatedName, - type: PageLayoutType.DASHBOARD, - }; - - const response = await makeRestAPIRequest({ - method: 'patch', - path: `/metadata/pageLayouts/${pageLayout.id}`, - body: updateData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - assertPageLayoutStructure(response.body, { - id: pageLayout.id, - name: updatedName, - type: PageLayoutType.DASHBOARD, - objectMetadataId: testObjectMetadataId, - }); - - await deleteTestPageLayoutWithRestApi(pageLayout.id); - }); - - it('should update only provided fields', async () => { - const originalName = generateRecordName('Original Page Layout'); - const pageLayout = await createTestPageLayoutWithRestApi({ - name: originalName, - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - const updatedName = generateRecordName('Updated Name Only'); - const updateData = { - name: updatedName, - }; - - const response = await makeRestAPIRequest({ - method: 'patch', - path: `/metadata/pageLayouts/${pageLayout.id}`, - body: updateData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - assertPageLayoutStructure(response.body, { - id: pageLayout.id, - name: updatedName, - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - await deleteTestPageLayoutWithRestApi(pageLayout.id); - }); - - it('should return 404 error when updating non-existent page layout', async () => { - const updateData = { - name: 'Updated Page Layout', - type: PageLayoutType.DASHBOARD, - }; - - const response = await makeRestAPIRequest({ - method: 'patch', - path: `/metadata/pageLayouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, - body: updateData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 404, - generatePageLayoutExceptionMessage( - PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - ), - ); - }); - }); - - describe('DELETE /rest/metadata/pageLayouts/:id', () => { - it('should delete an existing page layout', async () => { - const pageLayoutName = generateRecordName( - 'Test Page Layout for Delete', - ); - const pageLayout = await createTestPageLayoutWithRestApi({ - name: pageLayoutName, - type: PageLayoutType.RECORD_INDEX, - objectMetadataId: testObjectMetadataId, - }); - - const deleteResponse = await makeRestAPIRequest({ - method: 'delete', - path: `/metadata/pageLayouts/${pageLayout.id}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(deleteResponse); - assertPageLayoutStructure(deleteResponse.body, pageLayout); - - const getResponse = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayouts/${pageLayout.id}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - getResponse, - 404, - generatePageLayoutExceptionMessage( - PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND, - pageLayout.id, - ), - ); - }); - - it('should return 404 error when deleting non-existent page layout', async () => { - const response = await makeRestAPIRequest({ - method: 'delete', - path: `/metadata/pageLayouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiErrorResponse( - response, - 404, - generatePageLayoutExceptionMessage( - PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND, - TEST_NOT_EXISTING_PAGE_LAYOUT_ID, - ), - ); - }); - }); - - describe('Edge Cases', () => { - it('should handle multiple page layouts for same object', async () => { - const pageLayout1 = await createTestPageLayoutWithRestApi({ - name: generateRecordName('Page Layout 1'), - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: testObjectMetadataId, - }); - - const pageLayout2 = await createTestPageLayoutWithRestApi({ - name: generateRecordName('Page Layout 2'), - type: PageLayoutType.RECORD_INDEX, - objectMetadataId: testObjectMetadataId, - }); - - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/pageLayouts?objectMetadataId=${testObjectMetadataId}`, - bearer: API_KEY_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - expect(response.body).toHaveLength(2); - - await Promise.all([ - deleteTestPageLayoutWithRestApi(pageLayout1.id), - deleteTestPageLayoutWithRestApi(pageLayout2.id), - ]); - }); - - it('should handle page layouts with null objectMetadataId', async () => { - const pageLayout = await createTestPageLayoutWithRestApi({ - name: generateRecordName('Global Page Layout'), - type: PageLayoutType.DASHBOARD, - objectMetadataId: null, - }); - - assertPageLayoutStructure(pageLayout, { - type: PageLayoutType.DASHBOARD, - objectMetadataId: null, - }); - - await deleteTestPageLayoutWithRestApi(pageLayout.id); - }); - }); - }); -}); diff --git a/packages/twenty-server/test/integration/rest/utils/page-layout-rest-api.util.ts b/packages/twenty-server/test/integration/rest/utils/page-layout-rest-api.util.ts deleted file mode 100644 index d5bad2a42bb..00000000000 --- a/packages/twenty-server/test/integration/rest/utils/page-layout-rest-api.util.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; -import { generateRecordName } from 'test/integration/utils/generate-record-name'; - -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'; - -export const createTestPageLayoutWithRestApi = async ( - overrides: Partial = {}, -): Promise => { - const pageLayoutData = { - name: generateRecordName('Test Page Layout'), - type: PageLayoutType.RECORD_PAGE, - objectMetadataId: null, - ...overrides, - }; - - const response = await makeRestAPIRequest({ - method: 'post', - path: '/metadata/pageLayouts', - body: pageLayoutData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - if (response.status !== 201) { - throw new Error( - `Failed to create test page layout: ${response.status} - ${JSON.stringify(response.body)}`, - ); - } - - return response.body; -}; - -export const deleteTestPageLayoutWithRestApi = async ( - pageLayoutId: string, -): Promise => { - await makeRestAPIRequest({ - method: 'delete', - path: `/metadata/pageLayouts/${pageLayoutId}`, - bearer: API_KEY_ACCESS_TOKEN, - }).catch(() => {}); -}; diff --git a/packages/twenty-server/test/integration/rest/utils/page-layout-tab-rest-api.util.ts b/packages/twenty-server/test/integration/rest/utils/page-layout-tab-rest-api.util.ts deleted file mode 100644 index 0a8f98ba245..00000000000 --- a/packages/twenty-server/test/integration/rest/utils/page-layout-tab-rest-api.util.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; -import { generateRecordName } from 'test/integration/utils/generate-record-name'; - -import { type PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity'; - -export const createTestPageLayoutTabWithRestApi = async ( - overrides: Partial = {}, -): Promise => { - const pageLayoutTabData = { - title: generateRecordName('Test Page Layout Tab'), - position: 0, - ...overrides, - }; - - const response = await makeRestAPIRequest({ - method: 'post', - path: '/metadata/pageLayoutTabs', - body: pageLayoutTabData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - if (response.status !== 201) { - throw new Error( - `Failed to create test page layout tab: ${response.status} - ${JSON.stringify(response.body)}`, - ); - } - - return response.body; -}; - -export const deleteTestPageLayoutTabWithRestApi = async ( - pageLayoutTabId: string, -): Promise => { - await makeRestAPIRequest({ - method: 'delete', - path: `/metadata/pageLayoutTabs/${pageLayoutTabId}`, - bearer: API_KEY_ACCESS_TOKEN, - }); -}; diff --git a/packages/twenty-server/test/integration/rest/utils/page-layout-widget-rest-api.util.ts b/packages/twenty-server/test/integration/rest/utils/page-layout-widget-rest-api.util.ts deleted file mode 100644 index 1fbb7cc5258..00000000000 --- a/packages/twenty-server/test/integration/rest/utils/page-layout-widget-rest-api.util.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; -import { generateRecordName } from 'test/integration/utils/generate-record-name'; - -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'; - -export const createTestPageLayoutWidgetWithRestApi = async ( - overrides: Partial = {}, -): Promise => { - const pageLayoutWidgetData = { - title: generateRecordName('Test Page Layout Widget'), - type: WidgetType.VIEW, - gridPosition: { - row: 0, - column: 0, - rowSpan: 1, - columnSpan: 1, - }, - ...overrides, - }; - - const response = await makeRestAPIRequest({ - method: 'post', - path: '/metadata/pageLayoutWidgets', - body: pageLayoutWidgetData, - bearer: API_KEY_ACCESS_TOKEN, - }); - - if (response.status !== 201) { - throw new Error( - `Failed to create test page layout widget: ${response.status} - ${JSON.stringify(response.body)}`, - ); - } - - return response.body; -}; - -export const deleteTestPageLayoutWidgetWithRestApi = async ( - pageLayoutWidgetId: string, -): Promise => { - await makeRestAPIRequest({ - method: 'delete', - path: `/metadata/pageLayoutWidgets/${pageLayoutWidgetId}`, - bearer: API_KEY_ACCESS_TOKEN, - }); -}; diff --git a/packages/twenty-shared/src/utils/compute-diff-between-objects.ts b/packages/twenty-shared/src/utils/compute-diff-between-objects.ts index 78d652c043d..aef7c1f6762 100644 --- a/packages/twenty-shared/src/utils/compute-diff-between-objects.ts +++ b/packages/twenty-shared/src/utils/compute-diff-between-objects.ts @@ -30,7 +30,7 @@ type ComputeDiffBetweenObjectsParams< }; export const computeDiffBetweenObjects = < - T extends { id: string; deletedAt: Date | null }, + T extends { id: string; deletedAt: string | null }, K extends { id: string }, >({ existingObjects,