[DASHBOARDS] Allow dashboards to be restored (#17042)

This PR introduces a few changes:
- Add three actions: see deleted dashboards, destroy dashboard and
restore dashboard
- Remove the soft delete and restore on all the page layout entities
- Cascade the destruction of a dashboard to a page layout

Video QA:


https://github.com/user-attachments/assets/ab993b11-dd9c-4e88-880c-92691a521cc2
This commit is contained in:
Raphaël Bosi
2026-01-12 13:59:17 +01:00
committed by GitHub
parent 3ada8e5168
commit 655f1eef5f
60 changed files with 1178 additions and 2067 deletions
@@ -1918,9 +1918,6 @@ export type Mutation = {
deleteOneRole: Scalars['String'];
deleteOneRouteTrigger: RouteTrigger;
deleteOneServerlessFunction: ServerlessFunction;
deletePageLayout: PageLayout;
deletePageLayoutTab: Scalars['Boolean'];
deletePageLayoutWidget: PageLayoutWidget;
deletePublicDomain: Scalars['Boolean'];
deleteRowLevelPermissionPredicate: RowLevelPermissionPredicate;
deleteRowLevelPermissionPredicateGroup: RowLevelPermissionPredicateGroup;
@@ -1968,9 +1965,6 @@ export type Mutation = {
renewToken: AuthTokens;
resendEmailVerificationToken: ResendEmailVerificationTokenOutput;
resendWorkspaceInvitation: SendInvitationsOutput;
restorePageLayout: PageLayout;
restorePageLayoutTab: PageLayoutTab;
restorePageLayoutWidget: PageLayoutWidget;
retryJobs: RetryJobsResponse;
revokeApiKey?: Maybe<ApiKey>;
runEvaluationInput: AgentTurn;
@@ -2397,21 +2391,6 @@ export type MutationDeleteOneServerlessFunctionArgs = {
};
export type MutationDeletePageLayoutArgs = {
id: Scalars['String'];
};
export type MutationDeletePageLayoutTabArgs = {
id: Scalars['String'];
};
export type MutationDeletePageLayoutWidgetArgs = {
id: Scalars['String'];
};
export type MutationDeletePublicDomainArgs = {
domain: Scalars['String'];
};
@@ -2631,21 +2610,6 @@ export type MutationResendWorkspaceInvitationArgs = {
};
export type MutationRestorePageLayoutArgs = {
id: Scalars['String'];
};
export type MutationRestorePageLayoutTabArgs = {
id: Scalars['String'];
};
export type MutationRestorePageLayoutWidgetArgs = {
id: Scalars['String'];
};
export type MutationRetryJobsArgs = {
jobIds: Array<Scalars['String']>;
queueName: Scalars['String'];
@@ -1897,9 +1897,6 @@ export type Mutation = {
deleteOneRole: Scalars['String'];
deleteOneRouteTrigger: RouteTrigger;
deleteOneServerlessFunction: ServerlessFunction;
deletePageLayout: PageLayout;
deletePageLayoutTab: Scalars['Boolean'];
deletePageLayoutWidget: PageLayoutWidget;
deletePublicDomain: Scalars['Boolean'];
deleteRowLevelPermissionPredicate: RowLevelPermissionPredicate;
deleteRowLevelPermissionPredicateGroup: RowLevelPermissionPredicateGroup;
@@ -1945,9 +1942,6 @@ export type Mutation = {
renewToken: AuthTokens;
resendEmailVerificationToken: ResendEmailVerificationTokenOutput;
resendWorkspaceInvitation: SendInvitationsOutput;
restorePageLayout: PageLayout;
restorePageLayoutTab: PageLayoutTab;
restorePageLayoutWidget: PageLayoutWidget;
retryJobs: RetryJobsResponse;
revokeApiKey?: Maybe<ApiKey>;
runWorkflowVersion: RunWorkflowVersionOutput;
@@ -2352,21 +2346,6 @@ export type MutationDeleteOneServerlessFunctionArgs = {
};
export type MutationDeletePageLayoutArgs = {
id: Scalars['String'];
};
export type MutationDeletePageLayoutTabArgs = {
id: Scalars['String'];
};
export type MutationDeletePageLayoutWidgetArgs = {
id: Scalars['String'];
};
export type MutationDeletePublicDomainArgs = {
domain: Scalars['String'];
};
@@ -2576,21 +2555,6 @@ export type MutationResendWorkspaceInvitationArgs = {
};
export type MutationRestorePageLayoutArgs = {
id: Scalars['String'];
};
export type MutationRestorePageLayoutTabArgs = {
id: Scalars['String'];
};
export type MutationRestorePageLayoutWidgetArgs = {
id: Scalars['String'];
};
export type MutationRetryJobsArgs = {
jobIds: Array<Scalars['String']>;
queueName: Scalars['String'];
@@ -104,6 +104,9 @@ export const DASHBOARD_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
SingleRecordActionKeys.RESTORE,
MultipleRecordsActionKeys.DELETE,
MultipleRecordsActionKeys.DESTROY,
MultipleRecordsActionKeys.RESTORE,
NoSelectionRecordActionKeys.SEE_DELETED_RECORDS,
NoSelectionRecordActionKeys.HIDE_DELETED_RECORDS,
SingleRecordActionKeys.EXPORT_FROM_RECORD_SHOW,
SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD,
SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD,
@@ -160,26 +163,38 @@ export const DASHBOARD_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
position: 14,
label: msg`Restore dashboard`,
},
[NoSelectionRecordActionKeys.GO_TO_WORKFLOWS]: {
[MultipleRecordsActionKeys.RESTORE]: {
position: 15,
label: msg`Restore dashboards`,
},
[NoSelectionRecordActionKeys.SEE_DELETED_RECORDS]: {
position: 22,
label: msg`See deleted dashboards`,
},
[NoSelectionRecordActionKeys.HIDE_DELETED_RECORDS]: {
position: 23,
label: msg`Hide deleted dashboards`,
},
[NoSelectionRecordActionKeys.GO_TO_WORKFLOWS]: {
position: 24,
},
[NoSelectionRecordActionKeys.GO_TO_PEOPLE]: {
position: 16,
position: 25,
},
[NoSelectionRecordActionKeys.GO_TO_COMPANIES]: {
position: 17,
position: 26,
},
[NoSelectionRecordActionKeys.GO_TO_OPPORTUNITIES]: {
position: 18,
position: 27,
},
[NoSelectionRecordActionKeys.GO_TO_SETTINGS]: {
position: 19,
position: 28,
},
[NoSelectionRecordActionKeys.GO_TO_TASKS]: {
position: 20,
position: 29,
},
[NoSelectionRecordActionKeys.GO_TO_NOTES]: {
position: 21,
position: 30,
},
},
});
@@ -1,44 +0,0 @@
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-tab/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(),
};
};
@@ -1,51 +0,0 @@
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-tab/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,
};
};
@@ -1,41 +0,0 @@
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 { type DeletePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/delete-page-layout-widget.input';
import {
PageLayoutWidgetException,
PageLayoutWidgetExceptionCode,
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
export const fromDeletePageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow = ({
deletePageLayoutWidgetInput: rawDeletePageLayoutWidgetInput,
flatPageLayoutWidgetMaps,
}: {
deletePageLayoutWidgetInput: DeletePageLayoutWidgetInput;
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
}): FlatPageLayoutWidget => {
const { id: pageLayoutWidgetId } = extractAndSanitizeObjectStringFields(
rawDeletePageLayoutWidgetInput,
['id'],
);
const existingFlatPageLayoutWidgetToDelete =
flatPageLayoutWidgetMaps.byId[pageLayoutWidgetId];
if (!isDefined(existingFlatPageLayoutWidgetToDelete)) {
throw new PageLayoutWidgetException(
t`Page layout widget to delete not found`,
PageLayoutWidgetExceptionCode.PAGE_LAYOUT_WIDGET_NOT_FOUND,
);
}
return {
...existingFlatPageLayoutWidgetToDelete,
deletedAt: new Date().toISOString(),
};
};
@@ -1,51 +0,0 @@
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-widget/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,
};
};
@@ -1,43 +0,0 @@
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(),
};
};
@@ -1,50 +0,0 @@
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,
};
};
@@ -52,10 +52,10 @@ export class PageLayoutTabController {
);
}
return this.pageLayoutTabService.findByPageLayoutId(
workspace.id,
return this.pageLayoutTabService.findByPageLayoutId({
workspaceId: workspace.id,
pageLayoutId,
);
});
}
@Get(':id')
@@ -64,7 +64,10 @@ export class PageLayoutTabController {
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutTabDTO | null> {
return this.pageLayoutTabService.findByIdOrThrow(id, workspace.id);
return this.pageLayoutTabService.findByIdOrThrow({
id,
workspaceId: workspace.id,
});
}
@Post()
@@ -73,7 +76,10 @@ export class PageLayoutTabController {
@Body() input: CreatePageLayoutTabInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutTabDTO> {
return this.pageLayoutTabService.create(input, workspace.id);
return this.pageLayoutTabService.create({
createPageLayoutTabInput: input,
workspaceId: workspace.id,
});
}
@Patch(':id')
@@ -83,7 +89,11 @@ export class PageLayoutTabController {
@Body() input: UpdatePageLayoutTabInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutTabDTO> {
return this.pageLayoutTabService.update(id, workspace.id, input);
return this.pageLayoutTabService.update({
id,
workspaceId: workspace.id,
updateData: input,
});
}
@Delete(':id')
@@ -91,7 +101,10 @@ export class PageLayoutTabController {
async delete(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutTabDTO> {
return this.pageLayoutTabService.delete(id, workspace.id);
): Promise<boolean> {
return this.pageLayoutTabService.destroy({
id,
workspaceId: workspace.id,
});
}
}
@@ -7,7 +7,6 @@ import {
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
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 { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -36,10 +35,10 @@ export class PageLayoutTabResolver {
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('pageLayoutId', { type: () => String }) pageLayoutId: string,
): Promise<PageLayoutTabDTO[]> {
return this.pageLayoutTabService.findByPageLayoutId(
workspace.id,
return this.pageLayoutTabService.findByPageLayoutId({
workspaceId: workspace.id,
pageLayoutId,
);
});
}
@Query(() => PageLayoutTabDTO)
@@ -48,7 +47,10 @@ export class PageLayoutTabResolver {
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutTabDTO> {
return this.pageLayoutTabService.findByIdOrThrow(id, workspace.id);
return this.pageLayoutTabService.findByIdOrThrow({
id,
workspaceId: workspace.id,
});
}
@Mutation(() => PageLayoutTabDTO)
@@ -57,7 +59,10 @@ export class PageLayoutTabResolver {
@Args('input') input: CreatePageLayoutTabInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutTabDTO> {
return this.pageLayoutTabService.create(input, workspace.id);
return this.pageLayoutTabService.create({
createPageLayoutTabInput: input,
workspaceId: workspace.id,
});
}
@Mutation(() => PageLayoutTabDTO)
@@ -67,21 +72,11 @@ export class PageLayoutTabResolver {
@Args('input') input: UpdatePageLayoutTabInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutTabDTO> {
return this.pageLayoutTabService.update(id, workspace.id, input);
}
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async deletePageLayoutTab(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<boolean> {
const deletedPageLayoutTab = await this.pageLayoutTabService.delete(
return this.pageLayoutTabService.update({
id,
workspace.id,
);
return isDefined(deletedPageLayoutTab);
workspaceId: workspace.id,
updateData: input,
});
}
@Mutation(() => Boolean)
@@ -90,15 +85,9 @@ export class PageLayoutTabResolver {
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<boolean> {
return this.pageLayoutTabService.destroy(id, workspace.id);
}
@Mutation(() => PageLayoutTabDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async restorePageLayoutTab(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutTabDTO> {
return this.pageLayoutTabService.restore(id, workspace.id);
return this.pageLayoutTabService.destroy({
id,
workspaceId: workspace.id,
});
}
}
@@ -7,9 +7,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat
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,
@@ -40,10 +38,13 @@ export class PageLayoutTabService {
private readonly dashboardSyncService: DashboardSyncService,
) {}
async findByPageLayoutId(
workspaceId: string,
pageLayoutId: string,
): Promise<PageLayoutTabDTO[]> {
async findByPageLayoutId({
workspaceId,
pageLayoutId,
}: {
workspaceId: string;
pageLayoutId: string;
}): Promise<PageLayoutTabDTO[]> {
const { flatPageLayoutTabMaps, flatPageLayoutWidgetMaps } =
await this.getPageLayoutTabFlatEntityMaps(workspaceId);
@@ -63,10 +64,13 @@ export class PageLayoutTabService {
);
}
async findByIdOrThrow(
id: string,
workspaceId: string,
): Promise<PageLayoutTabDTO> {
async findByIdOrThrow({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<PageLayoutTabDTO> {
const { flatPageLayoutTabMaps, flatPageLayoutWidgetMaps } =
await this.getPageLayoutTabFlatEntityMaps(workspaceId);
@@ -102,10 +106,13 @@ export class PageLayoutTabService {
);
}
async create(
createPageLayoutTabInput: CreatePageLayoutTabInput,
workspaceId: string,
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
async create({
createPageLayoutTabInput,
workspaceId,
}: {
createPageLayoutTabInput: CreatePageLayoutTabInput;
workspaceId: string;
}): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
if (!isDefined(createPageLayoutTabInput.title)) {
throw new PageLayoutTabException(
generatePageLayoutTabExceptionMessage(
@@ -180,11 +187,15 @@ export class PageLayoutTabService {
return fromFlatPageLayoutTabToPageLayoutTabDto(createdTab);
}
async update(
id: string,
workspaceId: string,
updateData: UpdatePageLayoutTabInput,
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
async update({
id,
workspaceId,
updateData,
}: {
id: string;
workspaceId: string;
updateData: UpdatePageLayoutTabInput;
}): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
const { flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -248,69 +259,13 @@ export class PageLayoutTabService {
return fromFlatPageLayoutTabToPageLayoutTabDto(updatedTab);
}
async delete(
id: string,
workspaceId: string,
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
const { flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutTabMaps'],
},
);
const flatPageLayoutTabToDelete =
fromDeletePageLayoutTabInputToFlatPageLayoutTabOrThrow({
deletePageLayoutTabInput: { id },
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutTab: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatPageLayoutTabToDelete],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting page layout tab',
);
}
const { flatPageLayoutTabMaps: recomputedFlatPageLayoutTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutTabMaps'],
},
);
const deletedTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedFlatPageLayoutTabMaps,
});
await this.dashboardSyncService.updateLinkedDashboardsUpdatedAtByTabId({
tabId: id,
workspaceId,
updatedAt: new Date(deletedTab.updatedAt),
});
return fromFlatPageLayoutTabToPageLayoutTabDto(deletedTab);
}
async destroy(id: string, workspaceId: string): Promise<boolean> {
async destroy({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<boolean> {
const { flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -355,66 +310,4 @@ export class PageLayoutTabService {
return true;
}
async restore(
id: string,
workspaceId: string,
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
const { flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutTabMaps'],
},
);
const flatPageLayoutTabToRestore =
fromRestorePageLayoutTabInputToFlatPageLayoutTabOrThrow({
restorePageLayoutTabInput: { id },
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutTab: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatPageLayoutTabToRestore],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while restoring page layout tab',
);
}
const { flatPageLayoutTabMaps: recomputedFlatPageLayoutTabMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutTabMaps'],
},
);
const restoredTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedFlatPageLayoutTabMaps,
});
await this.dashboardSyncService.updateLinkedDashboardsUpdatedAtByTabId({
tabId: id,
workspaceId,
updatedAt: new Date(restoredTab.updatedAt),
});
return fromFlatPageLayoutTabToPageLayoutTabDto(restoredTab);
}
}
@@ -54,10 +54,10 @@ export class PageLayoutWidgetController {
);
}
return this.pageLayoutWidgetService.findByPageLayoutTabId(
workspace.id,
return this.pageLayoutWidgetService.findByPageLayoutTabId({
workspaceId: workspace.id,
pageLayoutTabId,
);
});
}
@Get(':id')
@@ -66,7 +66,10 @@ export class PageLayoutWidgetController {
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO | null> {
return this.pageLayoutWidgetService.findByIdOrThrow(id, workspace.id);
return this.pageLayoutWidgetService.findByIdOrThrow({
id,
workspaceId: workspace.id,
});
}
@Post()
@@ -75,7 +78,10 @@ export class PageLayoutWidgetController {
@Body() input: CreatePageLayoutWidgetInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.create(input, workspace.id);
return this.pageLayoutWidgetService.create({
input,
workspaceId: workspace.id,
});
}
@Patch(':id')
@@ -85,7 +91,11 @@ export class PageLayoutWidgetController {
@Body() input: UpdatePageLayoutWidgetInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.update(id, workspace.id, input);
return this.pageLayoutWidgetService.update({
id,
workspaceId: workspace.id,
updateData: input,
});
}
@Delete(':id')
@@ -93,7 +103,10 @@ export class PageLayoutWidgetController {
async delete(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.delete(id, workspace.id);
): Promise<boolean> {
return this.pageLayoutWidgetService.destroy({
id,
workspaceId: workspace.id,
});
}
}
@@ -45,10 +45,10 @@ export class PageLayoutWidgetResolver {
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('pageLayoutTabId', { type: () => String }) pageLayoutTabId: string,
): Promise<PageLayoutWidgetDTO[]> {
return this.pageLayoutWidgetService.findByPageLayoutTabId(
workspace.id,
return this.pageLayoutWidgetService.findByPageLayoutTabId({
workspaceId: workspace.id,
pageLayoutTabId,
);
});
}
@Query(() => PageLayoutWidgetDTO)
@@ -57,7 +57,10 @@ export class PageLayoutWidgetResolver {
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.findByIdOrThrow(id, workspace.id);
return this.pageLayoutWidgetService.findByIdOrThrow({
id,
workspaceId: workspace.id,
});
}
@Mutation(() => PageLayoutWidgetDTO)
@@ -66,7 +69,10 @@ export class PageLayoutWidgetResolver {
@Args('input') input: CreatePageLayoutWidgetInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.create(input, workspace.id);
return this.pageLayoutWidgetService.create({
input,
workspaceId: workspace.id,
});
}
@Mutation(() => PageLayoutWidgetDTO)
@@ -76,16 +82,11 @@ export class PageLayoutWidgetResolver {
@Args('input') input: UpdatePageLayoutWidgetInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.update(id, workspace.id, input);
}
@Mutation(() => PageLayoutWidgetDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async deletePageLayoutWidget(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.delete(id, workspace.id);
return this.pageLayoutWidgetService.update({
id,
workspaceId: workspace.id,
updateData: input,
});
}
@Mutation(() => Boolean)
@@ -94,16 +95,10 @@ export class PageLayoutWidgetResolver {
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<boolean> {
return this.pageLayoutWidgetService.destroy(id, workspace.id);
}
@Mutation(() => PageLayoutWidgetDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async restorePageLayoutWidget(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutWidgetDTO> {
return this.pageLayoutWidgetService.restore(id, workspace.id);
return this.pageLayoutWidgetService.destroy({
id,
workspaceId: workspace.id,
});
}
@ResolveField(() => WidgetConfiguration, { nullable: true })
@@ -8,9 +8,7 @@ import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-m
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,
@@ -87,10 +85,13 @@ export class PageLayoutWidgetService {
}
}
async findByPageLayoutTabId(
workspaceId: string,
pageLayoutTabId: string,
): Promise<PageLayoutWidgetDTO[]> {
async findByPageLayoutTabId({
workspaceId,
pageLayoutTabId,
}: {
workspaceId: string;
pageLayoutTabId: string;
}): Promise<PageLayoutWidgetDTO[]> {
const flatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
@@ -109,10 +110,13 @@ export class PageLayoutWidgetService {
.map(fromFlatPageLayoutWidgetToPageLayoutWidgetDto);
}
async findByIdOrThrow(
id: string,
workspaceId: string,
): Promise<PageLayoutWidgetDTO> {
async findByIdOrThrow({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<PageLayoutWidgetDTO> {
const flatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
@@ -131,10 +135,13 @@ export class PageLayoutWidgetService {
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(flatWidget);
}
async create(
createPageLayoutWidgetInput: CreatePageLayoutWidgetInput,
workspaceId: string,
): Promise<PageLayoutWidgetDTO> {
async create({
input,
workspaceId,
}: {
input: CreatePageLayoutWidgetInput;
workspaceId: string;
}): Promise<PageLayoutWidgetDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
@@ -142,7 +149,7 @@ export class PageLayoutWidgetService {
const flatPageLayoutWidgetToCreate =
fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate({
createPageLayoutWidgetInput,
createPageLayoutWidgetInput: input,
workspaceId,
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
});
@@ -174,11 +181,15 @@ export class PageLayoutWidgetService {
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(createdWidget);
}
async update(
id: string,
workspaceId: string,
updateData: UpdatePageLayoutWidgetInput,
): Promise<PageLayoutWidgetDTO> {
async update({
id,
workspaceId,
updateData,
}: {
id: string;
workspaceId: string;
updateData: UpdatePageLayoutWidgetInput;
}): Promise<PageLayoutWidgetDTO> {
const existingFlatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
@@ -252,44 +263,13 @@ export class PageLayoutWidgetService {
return existingWidget;
}
async delete(id: string, workspaceId: string): Promise<PageLayoutWidgetDTO> {
const existingFlatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
const flatPageLayoutWidgetToDelete =
fromDeletePageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow({
deletePageLayoutWidgetInput: { id },
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
});
await this.validateAndRunWidgetMigration({
workspaceId,
operations: {
flatEntityToCreate: [],
flatEntityToUpdate: [flatPageLayoutWidgetToDelete],
flatEntityToDelete: [],
},
errorMessage:
'Multiple validation errors occurred while deleting page layout widget',
});
const recomputedMaps = await this.getFlatPageLayoutWidgetMaps(workspaceId);
const deletedWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedMaps,
});
await this.dashboardSyncService.updateLinkedDashboardsUpdatedAtByWidgetId({
widgetId: id,
workspaceId,
updatedAt: new Date(deletedWidget.updatedAt),
});
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(deletedWidget);
}
async destroy(id: string, workspaceId: string): Promise<boolean> {
async destroy({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<boolean> {
const existingFlatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
@@ -318,41 +298,4 @@ export class PageLayoutWidgetService {
return true;
}
async restore(id: string, workspaceId: string): Promise<PageLayoutWidgetDTO> {
const existingFlatPageLayoutWidgetMaps =
await this.getFlatPageLayoutWidgetMaps(workspaceId);
const flatPageLayoutWidgetToRestore =
fromRestorePageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow({
restorePageLayoutWidgetInput: { id },
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
});
await this.validateAndRunWidgetMigration({
workspaceId,
operations: {
flatEntityToCreate: [],
flatEntityToUpdate: [flatPageLayoutWidgetToRestore],
flatEntityToDelete: [],
},
errorMessage:
'Multiple validation errors occurred while restoring page layout widget',
});
const recomputedMaps = await this.getFlatPageLayoutWidgetMaps(workspaceId);
const restoredWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedMaps,
});
await this.dashboardSyncService.updateLinkedDashboardsUpdatedAtByWidgetId({
widgetId: id,
workspaceId,
updatedAt: new Date(restoredWidget.updatedAt),
});
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(restoredWidget);
}
}
@@ -38,10 +38,10 @@ export class PageLayoutController {
@Query('objectMetadataId') objectMetadataId?: string,
): Promise<PageLayoutDTO[]> {
if (isDefined(objectMetadataId)) {
return this.pageLayoutService.findByObjectMetadataId(
workspace.id,
return this.pageLayoutService.findByObjectMetadataId({
workspaceId: workspace.id,
objectMetadataId,
);
});
}
return this.pageLayoutService.findByWorkspaceId(workspace.id);
@@ -53,7 +53,10 @@ export class PageLayoutController {
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutDTO | null> {
return this.pageLayoutService.findByIdOrThrow(id, workspace.id);
return this.pageLayoutService.findByIdOrThrow({
id,
workspaceId: workspace.id,
});
}
@Post()
@@ -62,7 +65,10 @@ export class PageLayoutController {
@Body() input: CreatePageLayoutInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutDTO> {
return this.pageLayoutService.create(input, workspace.id);
return this.pageLayoutService.create({
createPageLayoutInput: input,
workspaceId: workspace.id,
});
}
@Patch(':id')
@@ -72,26 +78,24 @@ export class PageLayoutController {
@Body() input: UpdatePageLayoutInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutDTO> {
const updatedPageLayout = await this.pageLayoutService.update(
const updatedPageLayout = await this.pageLayoutService.update({
id,
workspace.id,
input,
);
workspaceId: workspace.id,
updateData: input,
});
return updatedPageLayout;
}
@Delete(':id')
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async delete(
async destroy(
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutDTO> {
const deletedPageLayout = await this.pageLayoutService.delete(
): Promise<boolean> {
return this.pageLayoutService.destroy({
id,
workspace.id,
);
return deletedPageLayout;
workspaceId: workspace.id,
});
}
}
@@ -9,8 +9,6 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata
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 { PageLayoutTabModule } from 'src/engine/metadata-modules/page-layout-tab/page-layout-tab.module';
import { PageLayoutWidgetModule } from 'src/engine/metadata-modules/page-layout-widget/page-layout-widget.module';
import { PageLayoutController } from 'src/engine/metadata-modules/page-layout/controllers/page-layout.controller';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { PageLayoutResolver } from 'src/engine/metadata-modules/page-layout/resolvers/page-layout.resolver';
@@ -38,8 +36,6 @@ import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.m
FlatPageLayoutTabModule,
FlatPageLayoutWidgetModule,
ApplicationModule,
PageLayoutTabModule,
PageLayoutWidgetModule,
DashboardSyncModule,
],
controllers: [PageLayoutController],
@@ -7,7 +7,6 @@ import {
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
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';
@@ -43,10 +42,10 @@ export class PageLayoutResolver {
objectMetadataId?: string,
): Promise<PageLayoutDTO[]> {
if (objectMetadataId) {
return this.pageLayoutService.findByObjectMetadataId(
workspace.id,
return this.pageLayoutService.findByObjectMetadataId({
workspaceId: workspace.id,
objectMetadataId,
);
});
}
return this.pageLayoutService.findByWorkspaceId(workspace.id);
@@ -58,7 +57,10 @@ export class PageLayoutResolver {
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutDTO | null> {
return this.pageLayoutService.findByIdOrThrow(id, workspace.id);
return this.pageLayoutService.findByIdOrThrow({
id,
workspaceId: workspace.id,
});
}
@Mutation(() => PageLayoutDTO)
@@ -67,7 +69,10 @@ export class PageLayoutResolver {
@Args('input') input: CreatePageLayoutInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutDTO> {
return this.pageLayoutService.create(input, workspace.id);
return this.pageLayoutService.create({
createPageLayoutInput: input,
workspaceId: workspace.id,
});
}
@Mutation(() => PageLayoutDTO)
@@ -77,21 +82,11 @@ export class PageLayoutResolver {
@Args('input') input: UpdatePageLayoutInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutDTO> {
return this.pageLayoutService.update(id, workspace.id, input);
}
@Mutation(() => PageLayoutDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async deletePageLayout(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutDTO> {
const deletedPageLayout = await this.pageLayoutService.delete(
return this.pageLayoutService.update({
id,
workspace.id,
);
return deletedPageLayout;
workspaceId: workspace.id,
updateData: input,
});
}
@Mutation(() => Boolean)
@@ -100,21 +95,10 @@ export class PageLayoutResolver {
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<boolean> {
const deletedPageLayout = await this.pageLayoutService.destroy(
return this.pageLayoutService.destroy({
id,
workspace.id,
);
return isDefined(deletedPageLayout);
}
@Mutation(() => PageLayoutDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
async restorePageLayout(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<PageLayoutDTO> {
return this.pageLayoutService.restore(id, workspace.id);
workspaceId: workspace.id,
});
}
@Mutation(() => PageLayoutDTO)
@@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
@@ -10,9 +10,7 @@ import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-pag
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,
@@ -38,8 +36,6 @@ import { DashboardSyncService } from 'src/modules/dashboard-sync/services/dashbo
@Injectable()
export class PageLayoutService {
private readonly logger = new Logger(PageLayoutService.name);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
@@ -70,10 +66,13 @@ export class PageLayoutService {
);
}
async findByObjectMetadataId(
workspaceId: string,
objectMetadataId: string,
): Promise<PageLayoutDTO[]> {
async findByObjectMetadataId({
workspaceId,
objectMetadataId,
}: {
workspaceId: string;
objectMetadataId: string;
}): Promise<PageLayoutDTO[]> {
const {
flatPageLayoutMaps,
flatPageLayoutTabMaps,
@@ -99,10 +98,13 @@ export class PageLayoutService {
);
}
async findByIdOrThrow(
id: string,
workspaceId: string,
): Promise<PageLayoutDTO> {
async findByIdOrThrow({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<PageLayoutDTO> {
const {
flatPageLayoutMaps,
flatPageLayoutTabMaps,
@@ -111,7 +113,10 @@ export class PageLayoutService {
const flatLayout = flatPageLayoutMaps.byId[id];
if (!isDefined(flatLayout) || isDefined(flatLayout.deletedAt)) {
const isLayoutNotFound =
!isDefined(flatLayout) || isDefined(flatLayout.deletedAt);
if (isLayoutNotFound) {
throw new PageLayoutException(
generatePageLayoutExceptionMessage(
PageLayoutExceptionMessageKey.PAGE_LAYOUT_NOT_FOUND,
@@ -147,10 +152,13 @@ export class PageLayoutService {
);
}
async create(
createPageLayoutInput: CreatePageLayoutInput,
workspaceId: string,
): Promise<Omit<PageLayoutDTO, 'tabs'>> {
async create({
createPageLayoutInput,
workspaceId,
}: {
createPageLayoutInput: CreatePageLayoutInput;
workspaceId: string;
}): Promise<Omit<PageLayoutDTO, 'tabs'>> {
if (!isNonEmptyString(createPageLayoutInput.name)) {
throw new PageLayoutException(
generatePageLayoutExceptionMessage(
@@ -210,11 +218,15 @@ export class PageLayoutService {
);
}
async update(
id: string,
workspaceId: string,
updateData: UpdatePageLayoutInput,
): Promise<Omit<PageLayoutDTO, 'tabs'>> {
async update({
id,
workspaceId,
updateData,
}: {
id: string;
workspaceId: string;
updateData: UpdatePageLayoutInput;
}): Promise<Omit<PageLayoutDTO, 'tabs'>> {
const { flatPageLayoutMaps: existingFlatPageLayoutMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -280,74 +292,15 @@ export class PageLayoutService {
return fromFlatPageLayoutToPageLayoutDto(updatedLayout);
}
async delete(
id: string,
workspaceId: string,
): Promise<Omit<PageLayoutDTO, 'tabs'>> {
const { flatPageLayoutMaps: existingFlatPageLayoutMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutMaps'],
},
);
const flatPageLayoutToDelete =
fromDeletePageLayoutInputToFlatPageLayoutOrThrow({
deletePageLayoutInput: { id },
flatPageLayoutMaps: existingFlatPageLayoutMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatPageLayoutToDelete],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting page layout',
);
}
const { flatPageLayoutMaps: recomputedFlatPageLayoutMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutMaps'],
},
);
const deletedLayout = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedFlatPageLayoutMaps,
});
await this.dashboardSyncService.softDeleteLinkedDashboardsByPageLayoutId({
pageLayoutId: id,
workspaceId,
deletedAt: isDefined(deletedLayout.deletedAt)
? new Date(deletedLayout.deletedAt)
: new Date(),
});
return fromFlatPageLayoutToPageLayoutDto(deletedLayout);
}
async destroy(
id: string,
workspaceId: string,
): Promise<Omit<PageLayoutDTO, 'tabs'>> {
async destroy({
id,
workspaceId,
isLinkedDashboardAlreadyDestroyed = false,
}: {
id: string;
workspaceId: string;
isLinkedDashboardAlreadyDestroyed?: boolean;
}): Promise<boolean> {
const { flatPageLayoutMaps: existingFlatPageLayoutMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -384,106 +337,48 @@ export class PageLayoutService {
);
}
if (flatPageLayoutToDestroy.type === PageLayoutType.DASHBOARD) {
await this.destroyAssociatedDashboards(id, workspaceId);
if (
flatPageLayoutToDestroy.type === PageLayoutType.DASHBOARD &&
!isLinkedDashboardAlreadyDestroyed
) {
await this.destroyAssociatedDashboards({
pageLayoutId: id,
workspaceId,
});
}
return fromFlatPageLayoutToPageLayoutDto(flatPageLayoutToDestroy);
return true;
}
private async destroyAssociatedDashboards(
pageLayoutId: string,
workspaceId: string,
): Promise<void> {
private async destroyAssociatedDashboards({
pageLayoutId,
workspaceId,
}: {
pageLayoutId: string;
workspaceId: string;
}): Promise<void> {
const authContext = buildSystemAuthContext(workspaceId);
try {
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const dashboardRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'dashboard',
{ shouldBypassPermissionChecks: true },
);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const dashboardRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'dashboard',
{ shouldBypassPermissionChecks: true },
);
const dashboards = await dashboardRepository.find({
where: {
pageLayoutId,
},
});
for (const dashboard of dashboards) {
await dashboardRepository.delete(dashboard.id);
}
},
);
} catch (error) {
this.logger.error(
`Failed to destroy associated dashboards for page layout ${pageLayoutId}: ${error}`,
);
}
}
async restore(
id: string,
workspaceId: string,
): Promise<Omit<PageLayoutDTO, 'tabs'>> {
const { flatPageLayoutMaps: existingFlatPageLayoutMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutMaps'],
},
);
const flatPageLayoutToRestore =
fromRestorePageLayoutInputToFlatPageLayoutOrThrow({
restorePageLayoutInput: { id },
flatPageLayoutMaps: existingFlatPageLayoutMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatPageLayoutToRestore],
},
const dashboards = await dashboardRepository.find({
where: {
pageLayoutId,
},
workspaceId,
isSystemBuild: false,
},
);
});
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while restoring page layout',
);
}
const { flatPageLayoutMaps: recomputedFlatPageLayoutMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPageLayoutMaps'],
},
);
const restoredLayout = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedFlatPageLayoutMaps,
});
await this.dashboardSyncService.restoreLinkedDashboardsByPageLayoutId({
pageLayoutId: id,
workspaceId,
});
return fromFlatPageLayoutToPageLayoutDto(restoredLayout);
for (const dashboard of dashboards) {
await dashboardRepository.delete(dashboard.id);
}
},
);
}
}
@@ -79,89 +79,6 @@ export class DashboardSyncService {
}
}
async softDeleteLinkedDashboardsByPageLayoutId({
pageLayoutId,
workspaceId,
deletedAt,
}: {
pageLayoutId: string;
workspaceId: string;
deletedAt: Date;
}): Promise<void> {
const isDashboard = await this.isPageLayoutOfTypeDashboard({
pageLayoutId,
workspaceId,
});
if (!isDashboard) {
return;
}
const authContext = buildSystemAuthContext(workspaceId);
try {
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const dashboardRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'dashboard',
{ shouldBypassPermissionChecks: true },
);
await dashboardRepository.update({ pageLayoutId }, { deletedAt });
},
);
} catch (error) {
this.logger.error(
`Failed to soft delete dashboards for page layout ${pageLayoutId}: ${error}`,
);
}
}
async restoreLinkedDashboardsByPageLayoutId({
pageLayoutId,
workspaceId,
}: {
pageLayoutId: string;
workspaceId: string;
}): Promise<void> {
const isDashboard = await this.isPageLayoutOfTypeDashboard({
pageLayoutId,
workspaceId,
});
if (!isDashboard) {
return;
}
const authContext = buildSystemAuthContext(workspaceId);
try {
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const dashboardRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'dashboard',
{ shouldBypassPermissionChecks: true },
);
await dashboardRepository.update(
{ pageLayoutId },
{ deletedAt: null },
);
},
);
} catch (error) {
this.logger.error(
`Failed to restore dashboards for page layout ${pageLayoutId}: ${error}`,
);
}
}
async updateLinkedDashboardsUpdatedAtByTabId({
tabId,
workspaceId,
@@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type CreateManyResolverArgs } 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 { DashboardToPageLayoutSyncService } from 'src/modules/dashboard/services/dashboard-to-page-layout-sync.service';
import { type DashboardWorkspaceEntity } from 'src/modules/dashboard/standard-objects/dashboard.workspace-entity';
@Injectable()
@WorkspaceQueryHook(`dashboard.createMany`)
export class DashboardCreateManyPreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly dashboardToPageLayoutSyncService: DashboardToPageLayoutSyncService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: CreateManyResolverArgs<DashboardWorkspaceEntity>,
): Promise<CreateManyResolverArgs<DashboardWorkspaceEntity>> {
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
for (const data of payload.data) {
if (isDefined(data.pageLayoutId)) {
continue;
}
const pageLayoutId =
await this.dashboardToPageLayoutSyncService.createPageLayoutForDashboard(
{
workspaceId: workspace.id,
},
);
data.pageLayoutId = pageLayoutId;
}
return payload;
}
}
@@ -8,9 +8,7 @@ import { type CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-res
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 { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
import { DashboardToPageLayoutSyncService } from 'src/modules/dashboard/services/dashboard-to-page-layout-sync.service';
import { type DashboardWorkspaceEntity } from 'src/modules/dashboard/standard-objects/dashboard.workspace-entity';
@Injectable()
@@ -19,8 +17,7 @@ export class DashboardCreateOnePreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly pageLayoutService: PageLayoutService,
private readonly pageLayoutTabService: PageLayoutTabService,
private readonly dashboardToPageLayoutSyncService: DashboardToPageLayoutSyncService,
) {}
async execute(
@@ -36,24 +33,12 @@ export class DashboardCreateOnePreQueryHook
return payload;
}
const pageLayout = await this.pageLayoutService.create(
{
type: PageLayoutType.DASHBOARD,
objectMetadataId: null,
name: 'Dashboard Layout',
},
workspace.id,
);
const pageLayoutId =
await this.dashboardToPageLayoutSyncService.createPageLayoutForDashboard({
workspaceId: workspace.id,
});
await this.pageLayoutTabService.create(
{
title: 'Tab 1',
pageLayoutId: pageLayout.id,
},
workspace.id,
);
payload.data.pageLayoutId = pageLayout.id;
payload.data.pageLayoutId = pageLayoutId;
return payload;
}
@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type DestroyManyResolverArgs } 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 { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { DashboardToPageLayoutSyncService } from 'src/modules/dashboard/services/dashboard-to-page-layout-sync.service';
@Injectable()
@WorkspaceQueryHook(`dashboard.destroyMany`)
export class DashboardDestroyManyPreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly dashboardToPageLayoutSyncService: DashboardToPageLayoutSyncService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: DestroyManyResolverArgs<{ id: { in: string[] } }>,
): Promise<DestroyManyResolverArgs<{ id: { in: string[] } }>> {
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
await this.dashboardToPageLayoutSyncService.destroyPageLayoutsForDashboards(
{
dashboardIds: payload.filter.id.in,
workspaceId: workspace.id,
},
);
return payload;
}
}
@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type DestroyOneResolverArgs } 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 { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { DashboardToPageLayoutSyncService } from 'src/modules/dashboard/services/dashboard-to-page-layout-sync.service';
@Injectable()
@WorkspaceQueryHook(`dashboard.destroyOne`)
export class DashboardDestroyOnePreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly dashboardToPageLayoutSyncService: DashboardToPageLayoutSyncService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: DestroyOneResolverArgs,
): Promise<DestroyOneResolverArgs> {
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
await this.dashboardToPageLayoutSyncService.destroyPageLayoutsForDashboards(
{
dashboardIds: [payload.id],
workspaceId: workspace.id,
},
);
return payload;
}
}
@@ -2,10 +2,21 @@ import { Module } from '@nestjs/common';
import { PageLayoutTabModule } from 'src/engine/metadata-modules/page-layout-tab/page-layout-tab.module';
import { PageLayoutModule } from 'src/engine/metadata-modules/page-layout/page-layout.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { DashboardCreateManyPreQueryHook } from 'src/modules/dashboard/query-hooks/dashboard-create-many.pre-query.hook';
import { DashboardCreateOnePreQueryHook } from 'src/modules/dashboard/query-hooks/dashboard-create-one.pre-query.hook';
import { DashboardDestroyManyPreQueryHook } from 'src/modules/dashboard/query-hooks/dashboard-destroy-many.pre-query.hook';
import { DashboardDestroyOnePreQueryHook } from 'src/modules/dashboard/query-hooks/dashboard-destroy-one.pre-query.hook';
import { DashboardToPageLayoutSyncService } from 'src/modules/dashboard/services/dashboard-to-page-layout-sync.service';
@Module({
imports: [PageLayoutModule, PageLayoutTabModule],
providers: [DashboardCreateOnePreQueryHook],
imports: [PageLayoutModule, PageLayoutTabModule, TwentyORMModule],
providers: [
DashboardToPageLayoutSyncService,
DashboardCreateOnePreQueryHook,
DashboardCreateManyPreQueryHook,
DashboardDestroyOnePreQueryHook,
DashboardDestroyManyPreQueryHook,
],
})
export class DashboardQueryHookModule {}
@@ -0,0 +1,86 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import type { DashboardWorkspaceEntity } from 'src/modules/dashboard/standard-objects/dashboard.workspace-entity';
@Injectable()
export class DashboardToPageLayoutSyncService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly pageLayoutService: PageLayoutService,
private readonly pageLayoutTabService: PageLayoutTabService,
) {}
public async createPageLayoutForDashboard({
workspaceId,
}: {
workspaceId: string;
}): Promise<string> {
const pageLayout = await this.pageLayoutService.create({
createPageLayoutInput: {
type: PageLayoutType.DASHBOARD,
objectMetadataId: null,
name: 'Dashboard Layout',
},
workspaceId,
});
await this.pageLayoutTabService.create({
createPageLayoutTabInput: {
title: 'Tab 1',
pageLayoutId: pageLayout.id,
},
workspaceId,
});
return pageLayout.id;
}
public async destroyPageLayoutsForDashboards({
dashboardIds,
workspaceId,
}: {
dashboardIds: string[];
workspaceId: string;
}): Promise<void> {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const dashboardRepository =
await this.globalWorkspaceOrmManager.getRepository<DashboardWorkspaceEntity>(
workspaceId,
'dashboard',
{ shouldBypassPermissionChecks: true },
);
const dashboards = await dashboardRepository.find({
where: {
id: In(dashboardIds),
},
withDeleted: true,
});
for (const dashboard of dashboards) {
if (!isDefined(dashboard.pageLayoutId)) {
continue;
}
await this.pageLayoutService.destroy({
id: dashboard.pageLayoutId,
workspaceId,
isLinkedDashboardAlreadyDestroyed: true,
});
}
},
);
}
}
@@ -52,10 +52,10 @@ See create_complete_dashboard for configuration examples.`,
configuration?: AllPageLayoutWidgetConfiguration;
}) => {
try {
const widget = await deps.pageLayoutWidgetService.create(
parameters as CreatePageLayoutWidgetInput,
context.workspaceId,
);
const widget = await deps.pageLayoutWidgetService.create({
input: parameters as CreatePageLayoutWidgetInput,
workspaceId: context.workspaceId,
});
return {
success: true,
@@ -101,28 +101,35 @@ AGGREGATION OPERATIONS: COUNT, SUM, AVG, MIN, MAX, COUNT_EMPTY, COUNT_NOT_EMPTY`
const tabTitle = parameters.tabTitle ?? 'Main';
const widgets = parameters.widgets ?? [];
const pageLayout = await deps.pageLayoutService.create(
{ name: parameters.title, type: PageLayoutType.DASHBOARD },
context.workspaceId,
);
const pageLayout = await deps.pageLayoutService.create({
createPageLayoutInput: {
name: parameters.title,
type: PageLayoutType.DASHBOARD,
},
workspaceId: context.workspaceId,
});
const pageLayoutTab = await deps.pageLayoutTabService.create(
{ title: tabTitle, pageLayoutId: pageLayout.id, position: 0 },
context.workspaceId,
);
const pageLayoutTab = await deps.pageLayoutTabService.create({
createPageLayoutTabInput: {
title: tabTitle,
pageLayoutId: pageLayout.id,
position: 0,
},
workspaceId: context.workspaceId,
});
const createdWidgets = [];
const widgetErrors = [];
for (const widget of widgets) {
try {
const createdWidget = await deps.pageLayoutWidgetService.create(
{
const createdWidget = await deps.pageLayoutWidgetService.create({
input: {
...widget,
pageLayoutTabId: pageLayoutTab.id,
} as CreatePageLayoutWidgetInput,
context.workspaceId,
);
workspaceId: context.workspaceId,
});
createdWidgets.push({
id: createdWidget.id,
@@ -18,15 +18,15 @@ export const createDeleteDashboardWidgetTool = (
inputSchema: deleteDashboardWidgetSchema,
execute: async (parameters: { widgetId: string }) => {
try {
const widget = await deps.pageLayoutWidgetService.findByIdOrThrow(
parameters.widgetId,
context.workspaceId,
);
const widget = await deps.pageLayoutWidgetService.findByIdOrThrow({
id: parameters.widgetId,
workspaceId: context.workspaceId,
});
await deps.pageLayoutWidgetService.destroy(
parameters.widgetId,
context.workspaceId,
);
await deps.pageLayoutWidgetService.destroy({
id: parameters.widgetId,
workspaceId: context.workspaceId,
});
return {
success: true,
@@ -55,10 +55,10 @@ export const createGetDashboardTool = (
};
}
const pageLayout = await deps.pageLayoutService.findByIdOrThrow(
dashboard.pageLayoutId,
context.workspaceId,
);
const pageLayout = await deps.pageLayoutService.findByIdOrThrow({
id: dashboard.pageLayoutId,
workspaceId: context.workspaceId,
});
const tabs =
pageLayout.tabs?.map((tab) => ({
@@ -57,11 +57,11 @@ Only provide fields you want to change - others remain unchanged.`,
Object.entries(updates).filter(([, value]) => isDefined(value)),
);
const widget = await deps.pageLayoutWidgetService.update(
widgetId,
context.workspaceId,
const widget = await deps.pageLayoutWidgetService.update({
id: widgetId,
workspaceId: context.workspaceId,
updateData,
);
});
return {
success: true,
@@ -6,19 +6,16 @@ import {
} from 'test/integration/metadata/suites/dashboard/utils/dashboard-graphql.util';
import { duplicateOneDashboard } from 'test/integration/metadata/suites/dashboard/utils/duplicate-one-dashboard.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 { 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 { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
type TestContext = {
id: string;
@@ -57,10 +54,8 @@ const SUCCESSFUL_TEST_CASES: EachTestingContext<TestContext>[] = [
describe('Dashboard duplication should succeed', () => {
let testPageLayoutId: string;
let testPageLayoutTabId: string;
let testPageLayoutWidgetId: string;
let testDashboardId: string;
let duplicatedDashboardId: string;
let duplicatedPageLayoutId: string;
let currentTestContextId: string;
const cleanup = async () => {
@@ -69,30 +64,6 @@ describe('Dashboard duplication should succeed', () => {
duplicatedDashboardId = '';
}
if (isNonEmptyString(duplicatedPageLayoutId)) {
await destroyOnePageLayout({
expectToFail: false,
input: { id: duplicatedPageLayoutId },
});
duplicatedPageLayoutId = '';
}
if (isNonEmptyString(testPageLayoutWidgetId)) {
await destroyOnePageLayoutWidget({
expectToFail: false,
input: { id: testPageLayoutWidgetId },
});
testPageLayoutWidgetId = '';
}
if (isNonEmptyString(testPageLayoutTabId)) {
await destroyOnePageLayoutTab({
expectToFail: false,
input: { id: testPageLayoutTabId },
});
testPageLayoutTabId = '';
}
if (isNonEmptyString(testDashboardId)) {
await destroyDashboardWithGraphQL(testDashboardId);
testDashboardId = '';
@@ -102,23 +73,13 @@ describe('Dashboard duplication should succeed', () => {
await destroyDashboardWithGraphQL(currentTestContextId);
currentTestContextId = '';
}
if (isNonEmptyString(testPageLayoutId)) {
await destroyOnePageLayout({
expectToFail: false,
input: { id: testPageLayoutId },
});
testPageLayoutId = '';
}
};
beforeEach(async () => {
testPageLayoutId = '';
testPageLayoutTabId = '';
testPageLayoutWidgetId = '';
testDashboardId = '';
duplicatedDashboardId = '';
duplicatedPageLayoutId = '';
currentTestContextId = '';
});
@@ -153,7 +114,7 @@ describe('Dashboard duplication should succeed', () => {
testPageLayoutTabId = tabData.createPageLayoutTab.id;
if (withWidgets) {
const { data: widgetData } = await createOnePageLayoutWidget({
await createOnePageLayoutWidget({
expectToFail: false,
input: {
title: 'Test Widget',
@@ -168,8 +129,6 @@ describe('Dashboard duplication should succeed', () => {
configuration: TEST_IFRAME_CONFIG,
},
});
testPageLayoutWidgetId = widgetData.createPageLayoutWidget.id;
}
}
@@ -187,7 +146,6 @@ describe('Dashboard duplication should succeed', () => {
});
duplicatedDashboardId = data.duplicateDashboard.id;
duplicatedPageLayoutId = data.duplicateDashboard.pageLayoutId ?? '';
expect(data.duplicateDashboard).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny({ ...data.duplicateDashboard }),
@@ -0,0 +1,168 @@
import { isNonEmptyString } from '@sniptt/guards';
import {
createManyDashboardsWithGraphQL,
createTestDashboardWithGraphQL,
destroyDashboardWithGraphQL,
destroyManyDashboardsWithGraphQL,
} from 'test/integration/metadata/suites/dashboard/utils/dashboard-graphql.util';
import { findPageLayoutTabs } from 'test/integration/metadata/suites/page-layout-tab/utils/find-page-layout-tabs.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 { findOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/find-one-page-layout.util';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
describe('Dashboard page layout auto-creation should succeed', () => {
describe('createOne dashboard without pageLayoutId', () => {
let createdDashboardId: string;
beforeEach(() => {
createdDashboardId = '';
});
afterEach(async () => {
if (isNonEmptyString(createdDashboardId)) {
await destroyDashboardWithGraphQL(createdDashboardId);
}
});
it('should auto-create page layout and tab for dashboard without pageLayoutId', async () => {
const title = 'Test Dashboard';
const dashboard = await createTestDashboardWithGraphQL({ title });
createdDashboardId = dashboard.id;
expect(dashboard.id).toBeDefined();
expect(dashboard.title).toBe(title);
expect(dashboard.pageLayoutId).toBeDefined();
expect(isNonEmptyString(dashboard.pageLayoutId)).toBe(true);
const { data: pageLayoutData } = await findOnePageLayout({
expectToFail: false,
input: { id: dashboard.pageLayoutId! },
});
expect(pageLayoutData.getPageLayout).toBeDefined();
expect(pageLayoutData.getPageLayout?.type).toBe(PageLayoutType.DASHBOARD);
const { data: tabsData } = await findPageLayoutTabs({
expectToFail: false,
input: { pageLayoutId: dashboard.pageLayoutId! },
});
expect(tabsData.getPageLayoutTabs).toBeDefined();
expect(tabsData.getPageLayoutTabs.length).toBeGreaterThanOrEqual(1);
expect(tabsData.getPageLayoutTabs[0].title).toBe('Tab 1');
});
});
describe('createMany dashboards without pageLayoutId', () => {
let createdDashboardIds: string[] = [];
beforeEach(() => {
createdDashboardIds = [];
});
afterEach(async () => {
if (createdDashboardIds.length > 0) {
await destroyManyDashboardsWithGraphQL({
id: { in: createdDashboardIds },
});
}
});
it('should auto-create separate page layouts for each dashboard in createMany', async () => {
const dashboardsData = [
{ title: 'Dashboard 1' },
{ title: 'Dashboard 2' },
{ title: 'Dashboard 3' },
];
const dashboards = await createManyDashboardsWithGraphQL(dashboardsData);
createdDashboardIds = dashboards.map((d) => d.id);
expect(dashboards.length).toBe(dashboardsData.length);
const pageLayoutIds = new Set(
dashboards.map((d) => d.pageLayoutId).filter(isNonEmptyString),
);
expect(pageLayoutIds.size).toBe(dashboardsData.length);
for (const dashboard of dashboards) {
expect(dashboard.pageLayoutId).toBeDefined();
expect(isNonEmptyString(dashboard.pageLayoutId)).toBe(true);
const { data: pageLayoutData } = await findOnePageLayout({
expectToFail: false,
input: { id: dashboard.pageLayoutId! },
});
expect(pageLayoutData.getPageLayout).toBeDefined();
expect(pageLayoutData.getPageLayout?.type).toBe(
PageLayoutType.DASHBOARD,
);
const { data: tabsData } = await findPageLayoutTabs({
expectToFail: false,
input: { pageLayoutId: dashboard.pageLayoutId! },
});
expect(tabsData.getPageLayoutTabs).toBeDefined();
expect(tabsData.getPageLayoutTabs.length).toBeGreaterThanOrEqual(1);
}
});
it('should use provided pageLayoutId for some dashboards and auto-create for others', async () => {
const { data: pageLayoutData } = await createOnePageLayout({
input: {
name: 'Pre-existing page layout',
type: PageLayoutType.DASHBOARD,
},
expectToFail: false,
});
const existingPageLayoutId = pageLayoutData.createPageLayout.id;
expect(existingPageLayoutId).toBeDefined();
const dashboardsData = [
{
title: 'Dashboard with provided layout',
pageLayoutId: existingPageLayoutId,
},
{ title: 'Dashboard without layout 1' },
{ title: 'Dashboard without layout 2' },
];
const dashboards = await createManyDashboardsWithGraphQL(dashboardsData);
createdDashboardIds = dashboards.map((d) => d.id);
expect(dashboards.length).toBe(3);
const dashboardWithProvidedLayout = dashboards.find(
(d) => d.title === 'Dashboard with provided layout',
);
const dashboardsWithAutoCreatedLayout = dashboards.filter(
(d) => d.title !== 'Dashboard with provided layout',
);
expect(dashboardWithProvidedLayout?.pageLayoutId).toBe(
existingPageLayoutId,
);
for (const dashboard of dashboardsWithAutoCreatedLayout) {
expect(dashboard.pageLayoutId).toBeDefined();
expect(isNonEmptyString(dashboard.pageLayoutId)).toBe(true);
expect(dashboard.pageLayoutId).not.toBe(existingPageLayoutId);
}
await destroyOnePageLayout({
input: { id: existingPageLayoutId },
expectToFail: false,
});
});
});
});
@@ -0,0 +1,118 @@
import { isNonEmptyString } from '@sniptt/guards';
import {
createManyDashboardsWithGraphQL,
createTestDashboardWithGraphQL,
destroyDashboardWithGraphQL,
destroyManyDashboardsWithGraphQL,
} from 'test/integration/metadata/suites/dashboard/utils/dashboard-graphql.util';
import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
import { findOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/find-one-page-layout.util';
describe('Dashboard to Page Layout sync should succeed', () => {
describe('destroyOne dashboard', () => {
let pageLayoutId: string;
beforeEach(() => {
pageLayoutId = '';
});
afterEach(async () => {
if (isNonEmptyString(pageLayoutId)) {
await destroyOnePageLayout({
expectToFail: false,
input: { id: pageLayoutId },
});
pageLayoutId = '';
}
});
it('should hard delete linked page layout when dashboard is destroyed', async () => {
const dashboard = await createTestDashboardWithGraphQL({
title: 'Dashboard for Destroy Test',
});
pageLayoutId = dashboard.pageLayoutId ?? '';
expect(isNonEmptyString(pageLayoutId)).toBe(true);
const { data: beforeData } = await findOnePageLayout({
expectToFail: false,
input: { id: pageLayoutId },
});
expect(beforeData.getPageLayout).toBeDefined();
await destroyDashboardWithGraphQL(dashboard.id);
const { errors } = await findOnePageLayout({
expectToFail: true,
input: { id: pageLayoutId },
});
expect(errors).toBeDefined();
pageLayoutId = '';
});
});
describe('destroyMany dashboards', () => {
let pageLayoutIds: string[] = [];
beforeEach(() => {
pageLayoutIds = [];
});
afterEach(async () => {
for (const pageLayoutId of pageLayoutIds) {
if (isNonEmptyString(pageLayoutId)) {
await destroyOnePageLayout({
expectToFail: false,
input: { id: pageLayoutId },
});
}
}
pageLayoutIds = [];
});
it('should hard delete all linked page layouts when multiple dashboards are destroyed', async () => {
const dashboardsData = [
{ title: 'Dashboard 1 for Destroy Many Test' },
{ title: 'Dashboard 2 for Destroy Many Test' },
];
const dashboards = await createManyDashboardsWithGraphQL(dashboardsData);
const dashboardIds = dashboards.map((d) => d.id);
pageLayoutIds = dashboards
.map((d) => d.pageLayoutId)
.filter(isNonEmptyString);
expect(pageLayoutIds.length).toBe(dashboardsData.length);
for (const pageLayoutId of pageLayoutIds) {
const { data } = await findOnePageLayout({
expectToFail: false,
input: { id: pageLayoutId },
});
expect(data.getPageLayout).toBeDefined();
}
await destroyManyDashboardsWithGraphQL({
id: { in: dashboardIds },
});
for (const pageLayoutId of pageLayoutIds) {
const { errors } = await findOnePageLayout({
expectToFail: true,
input: { id: pageLayoutId },
});
expect(errors).toBeDefined();
}
pageLayoutIds = [];
});
});
});
@@ -5,19 +5,13 @@ import {
findDashboardWithGraphQL,
} from 'test/integration/metadata/suites/dashboard/utils/dashboard-graphql.util';
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 { updateOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/update-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 { 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 { 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 { updateOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/update-one-page-layout.util';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
@@ -83,22 +77,7 @@ const createTestContext = async (): Promise<TestContext> => {
};
const cleanupTestContext = async (context: TestContext): Promise<void> => {
await destroyOnePageLayoutWidget({
expectToFail: false,
input: { id: context.widgetId },
});
await destroyOnePageLayoutTab({
expectToFail: false,
input: { id: context.tabId },
});
await destroyDashboardWithGraphQL(context.dashboardId);
await destroyOnePageLayout({
expectToFail: false,
input: { id: context.pageLayoutId },
});
};
const assertDashboardUpdatedAtIncreased = async (
@@ -124,43 +103,6 @@ const assertDashboardUpdatedAtIncreased = async (
expect(isIncreased).toBe(true);
};
const assertDashboardSoftDeleted = async (
dashboardId: string,
operation: () => Promise<void>,
): Promise<void> => {
const dashboardBefore = await findDashboardWithGraphQL(dashboardId);
expect(dashboardBefore).not.toBeNull();
await operation();
const dashboardAfter = await findDashboardWithGraphQL(dashboardId);
expect(dashboardAfter).toBeNull();
};
const assertDashboardRestored = async (
dashboardId: string,
operation: () => Promise<void>,
): Promise<void> => {
const dashboardBefore = await findDashboardWithGraphQL(dashboardId);
expect(dashboardBefore).toBeNull();
await operation();
const dashboardAfter = await findDashboardWithGraphQL(dashboardId);
expect(dashboardAfter).not.toBeNull();
const updatedAtAfter = new Date(dashboardAfter!.updatedAt);
const now = new Date();
const timeDiff = now.getTime() - updatedAtAfter.getTime();
expect(timeDiff).toBeLessThan(5000);
};
describe('Dashboard updatedAt should sync when linked page layout entities change', () => {
describe('Widget operations', () => {
let context: TestContext;
@@ -212,29 +154,6 @@ describe('Dashboard updatedAt should sync when linked page layout entities chang
});
});
});
it('should update dashboard updatedAt when widget is soft deleted', async () => {
await assertDashboardUpdatedAtIncreased(context.dashboardId, async () => {
await deleteOnePageLayoutWidget({
expectToFail: false,
input: { id: context.widgetId },
});
});
});
it('should update dashboard updatedAt when widget is restored', async () => {
await deleteOnePageLayoutWidget({
expectToFail: false,
input: { id: context.widgetId },
});
await assertDashboardUpdatedAtIncreased(context.dashboardId, async () => {
await restoreOnePageLayoutWidget({
expectToFail: false,
input: { id: context.widgetId },
});
});
});
});
describe('Tab operations', () => {
@@ -278,49 +197,6 @@ describe('Dashboard updatedAt should sync when linked page layout entities chang
});
});
});
it('should update dashboard updatedAt when tab is soft deleted', async () => {
const { data: tabData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: 'Tab to Delete',
pageLayoutId: context.pageLayoutId,
},
});
additionalTabId = tabData.createPageLayoutTab.id;
await assertDashboardUpdatedAtIncreased(context.dashboardId, async () => {
await deleteOnePageLayoutTab({
expectToFail: false,
input: { id: additionalTabId! },
});
});
});
it('should update dashboard updatedAt when tab is restored', async () => {
const { data: tabData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: 'Tab to Restore',
pageLayoutId: context.pageLayoutId,
},
});
additionalTabId = tabData.createPageLayoutTab.id;
await deleteOnePageLayoutTab({
expectToFail: false,
input: { id: additionalTabId },
});
await assertDashboardUpdatedAtIncreased(context.dashboardId, async () => {
await restoreOnePageLayoutTab({
expectToFail: false,
input: { id: additionalTabId! },
});
});
});
});
describe('Page layout operations', () => {
@@ -345,29 +221,6 @@ describe('Dashboard updatedAt should sync when linked page layout entities chang
});
});
});
it('should update dashboard updatedAt when page layout is soft deleted', async () => {
await assertDashboardSoftDeleted(context.dashboardId, async () => {
await deleteOnePageLayout({
expectToFail: false,
input: { id: context.pageLayoutId },
});
});
});
it('should update dashboard updatedAt when page layout is restored', async () => {
await deleteOnePageLayout({
expectToFail: false,
input: { id: context.pageLayoutId },
});
await assertDashboardRestored(context.dashboardId, async () => {
await restoreOnePageLayout({
expectToFail: false,
input: { id: context.pageLayoutId },
});
});
});
});
describe('Non-dashboard page layout operations should not trigger sync', () => {
@@ -10,10 +10,34 @@ interface CreateDashboardResponse extends Record<string, unknown> {
createDashboard: DashboardWorkspaceEntity;
}
interface CreateManyDashboardsResponse extends Record<string, unknown> {
createDashboards: DashboardWorkspaceEntity[];
}
interface FindDashboardResponse extends Record<string, unknown> {
dashboard: DashboardWorkspaceEntity | null;
}
interface DeleteDashboardResponse extends Record<string, unknown> {
deleteDashboard: DashboardWorkspaceEntity;
}
interface DeleteManyDashboardsResponse extends Record<string, unknown> {
deleteDashboards: DashboardWorkspaceEntity[];
}
interface RestoreDashboardResponse extends Record<string, unknown> {
restoreDashboard: DashboardWorkspaceEntity;
}
interface RestoreManyDashboardsResponse extends Record<string, unknown> {
restoreDashboards: DashboardWorkspaceEntity[];
}
interface DestroyManyDashboardsResponse extends Record<string, unknown> {
destroyDashboards: DashboardWorkspaceEntity[];
}
export const createTestDashboardWithGraphQL = async (data: {
id?: string;
title: string;
@@ -87,20 +111,259 @@ export const destroyDashboardWithGraphQL = async (
): Promise<void> => {
const operation = {
query: gql`
mutation DestroyDashboard($filter: DashboardFilterInput!) {
destroyDashboard(filter: $filter) {
mutation DestroyDashboard($dashboardId: UUID!) {
destroyDashboard(id: $dashboardId) {
id
}
}
`,
variables: {
filter: { id: { eq: dashboardId } },
dashboardId,
},
};
await makeGraphqlAPIRequest(operation);
};
export const createManyDashboardsWithGraphQL = async (
data: Array<{
id?: string;
title: string;
position?: number;
pageLayoutId?: string;
}>,
): Promise<DashboardWorkspaceEntity[]> => {
const operation = {
query: gql`
mutation CreateDashboards($data: [DashboardCreateInput!]!) {
createDashboards(data: $data) {
${DASHBOARD_GQL_FIELDS}
}
}
`,
variables: {
data: data.map((item, index) => ({
id: item.id,
title: item.title,
position: item.position ?? index,
pageLayoutId: item.pageLayoutId,
})),
},
};
const response = (await makeGraphqlAPIRequest(
operation,
)) as GraphQLResponse<CreateManyDashboardsResponse>;
if (response.body.errors) {
throw new Error(
`Failed to create dashboards: ${JSON.stringify(response.body.errors)}`,
);
}
if (!response.body.data) {
throw new Error('No data returned from createManyDashboardsWithGraphQL');
}
return response.body.data.createDashboards;
};
export const deleteDashboardWithGraphQL = async (
dashboardId: string,
): Promise<DashboardWorkspaceEntity> => {
const operation = {
query: gql`
mutation DeleteDashboard($dashboardId: UUID!) {
deleteDashboard(id: $dashboardId) {
${DASHBOARD_GQL_FIELDS}
}
}
`,
variables: {
dashboardId,
},
};
const response = (await makeGraphqlAPIRequest(
operation,
)) as GraphQLResponse<DeleteDashboardResponse>;
if (response.body.errors) {
throw new Error(
`Failed to delete dashboard: ${JSON.stringify(response.body.errors)}`,
);
}
if (!response.body.data) {
throw new Error('No data returned from deleteDashboardWithGraphQL');
}
return response.body.data.deleteDashboard;
};
export const deleteManyDashboardsWithGraphQL = async (filter: {
id: { in: string[] };
}): Promise<DashboardWorkspaceEntity[]> => {
const operation = {
query: gql`
mutation DeleteDashboards($filter: DashboardFilterInput!) {
deleteDashboards(filter: $filter) {
${DASHBOARD_GQL_FIELDS}
}
}
`,
variables: {
filter,
},
};
const response = (await makeGraphqlAPIRequest(
operation,
)) as GraphQLResponse<DeleteManyDashboardsResponse>;
if (response.body.errors) {
throw new Error(
`Failed to delete dashboards: ${JSON.stringify(response.body.errors)}`,
);
}
if (!response.body.data) {
throw new Error('No data returned from deleteManyDashboardsWithGraphQL');
}
return response.body.data.deleteDashboards;
};
export const restoreDashboardWithGraphQL = async (
dashboardId: string,
): Promise<DashboardWorkspaceEntity> => {
const operation = {
query: gql`
mutation RestoreDashboard($dashboardId: UUID!) {
restoreDashboard(id: $dashboardId) {
${DASHBOARD_GQL_FIELDS}
}
}
`,
variables: {
dashboardId,
},
};
const response = (await makeGraphqlAPIRequest(
operation,
)) as GraphQLResponse<RestoreDashboardResponse>;
if (response.body.errors) {
throw new Error(
`Failed to restore dashboard: ${JSON.stringify(response.body.errors)}`,
);
}
if (!response.body.data) {
throw new Error('No data returned from restoreDashboardWithGraphQL');
}
return response.body.data.restoreDashboard;
};
export const restoreManyDashboardsWithGraphQL = async (filter: {
id: { in: string[] };
}): Promise<DashboardWorkspaceEntity[]> => {
const operation = {
query: gql`
mutation RestoreDashboards($filter: DashboardFilterInput!) {
restoreDashboards(filter: $filter) {
${DASHBOARD_GQL_FIELDS}
}
}
`,
variables: {
filter,
},
};
const response = (await makeGraphqlAPIRequest(
operation,
)) as GraphQLResponse<RestoreManyDashboardsResponse>;
if (response.body.errors) {
throw new Error(
`Failed to restore dashboards: ${JSON.stringify(response.body.errors)}`,
);
}
if (!response.body.data) {
throw new Error('No data returned from restoreManyDashboardsWithGraphQL');
}
return response.body.data.restoreDashboards;
};
export const destroyManyDashboardsWithGraphQL = async (filter: {
id: { in: string[] };
}): Promise<DashboardWorkspaceEntity[]> => {
const operation = {
query: gql`
mutation DestroyDashboards($filter: DashboardFilterInput!) {
destroyDashboards(filter: $filter) {
${DASHBOARD_GQL_FIELDS}
}
}
`,
variables: {
filter,
},
};
const response = (await makeGraphqlAPIRequest(
operation,
)) as GraphQLResponse<DestroyManyDashboardsResponse>;
if (response.body.errors) {
throw new Error(
`Failed to destroy dashboards: ${JSON.stringify(response.body.errors)}`,
);
}
if (!response.body.data) {
throw new Error('No data returned from destroyManyDashboardsWithGraphQL');
}
return response.body.data.destroyDashboards;
};
export const findDeletedDashboardWithGraphQL = async (
dashboardId: string,
): Promise<DashboardWorkspaceEntity | null> => {
const operation = {
query: gql`
query FindDeletedDashboard($filter: DashboardFilterInput!) {
dashboard(filter: $filter) {
${DASHBOARD_GQL_FIELDS}
deletedAt
}
}
`,
variables: {
filter: {
id: { eq: dashboardId },
deletedAt: { is: 'NOT_NULL' },
},
},
};
const response = (await makeGraphqlAPIRequest(
operation,
)) as GraphQLResponse<FindDashboardResponse>;
if (response.body.errors) {
return null;
}
return response.body.data?.dashboard ?? null;
};
const TEST_SCHEMA_NAME = 'workspace_1wgvd1injqtife6y4rvfbu3h5';
export const cleanupDashboardRecords = async (): Promise<void> => {
@@ -1,12 +0,0 @@
// 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",
}
`;
@@ -1,13 +0,0 @@
// 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<String>,
"deletedAt": null,
"id": Any<String>,
"pageLayoutId": Any<String>,
"position": 0,
"title": "Tab To Delete",
"updatedAt": Any<String>,
}
`;
@@ -1,14 +0,0 @@
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 });
});
});
@@ -1,36 +1,7 @@
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<TestContext>[] = [
{
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;
@@ -51,50 +22,22 @@ describe('Page layout tab deletion should succeed', () => {
});
});
it.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'should $title',
async ({ context: { title, operation } }) => {
const { data: createData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title,
pageLayoutId: testPageLayoutId,
},
});
it('should hard delete a page layout tab', async () => {
const { data: createData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: 'Tab To Destroy',
pageLayoutId: testPageLayoutId,
},
});
const tabId = createData.createPageLayoutTab.id;
const tabId = createData.createPageLayoutTab.id;
if (operation === 'soft-delete-restore') {
const { data: deleteData } = await deleteOnePageLayoutTab({
expectToFail: false,
input: { id: tabId },
});
const { data: destroyData } = await destroyOnePageLayoutTab({
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);
}
},
);
expect(destroyData.destroyPageLayoutTab).toBe(true);
});
});
@@ -1,19 +0,0 @@
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<DeleteOnePageLayoutTabFactoryInput>) => ({
query: gql`
mutation DeletePageLayoutTab($id: String!) {
deletePageLayoutTab(id: $id)
}
`,
variables: {
id: input.id,
},
});
@@ -1,39 +0,0 @@
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<DeleteOnePageLayoutTabFactoryInput>): 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 };
};
@@ -1,32 +0,0 @@
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<RestoreOnePageLayoutTabFactoryInput>) => ({
query: gql`
mutation RestorePageLayoutTab($id: String!) {
restorePageLayoutTab(id: $id) {
${gqlFields}
}
}
`,
variables: {
id: input.id,
},
});
@@ -1,43 +0,0 @@
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-tab/dtos/page-layout-tab.dto';
export const restoreOnePageLayoutTab = async ({
input,
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<RestoreOnePageLayoutTabFactoryInput>): 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 };
};
@@ -1,12 +0,0 @@
// 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",
}
`;
@@ -1,47 +0,0 @@
// 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": {
"configurationType": "IFRAME",
"url": null,
},
"createdAt": Any<String>,
"deletedAt": Any<String>,
"gridPosition": {
"column": 0,
"columnSpan": 1,
"row": 0,
"rowSpan": 1,
},
"id": Any<String>,
"objectMetadataId": null,
"pageLayoutTabId": Any<String>,
"title": "Widget To Delete",
"type": "IFRAME",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget deletion should succeed should soft delete and restore a page layout widget 2`] = `
{
"configuration": {
"configurationType": "IFRAME",
"url": null,
},
"createdAt": Any<String>,
"deletedAt": null,
"gridPosition": {
"column": 0,
"columnSpan": 1,
"row": 0,
"rowSpan": 1,
},
"id": Any<String>,
"objectMetadataId": null,
"pageLayoutTabId": Any<String>,
"title": "Widget To Delete",
"type": "IFRAME",
"updatedAt": Any<String>,
}
`;
@@ -1,14 +0,0 @@
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 });
});
});
@@ -1,42 +1,13 @@
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';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
type TestContext = {
title: string;
operation: 'soft-delete-restore' | 'hard-delete';
};
const SUCCESSFUL_TEST_CASES: EachTestingContext<TestContext>[] = [
{
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;
@@ -71,64 +42,32 @@ describe('Page layout widget deletion should succeed', () => {
});
});
it.each(eachTestingContextFilter(SUCCESSFUL_TEST_CASES))(
'should $title',
async ({ context: { title, operation } }) => {
const { data: createData } = await createOnePageLayoutWidget({
expectToFail: false,
input: {
title,
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.IFRAME,
configuration: {
configurationType: WidgetConfigurationType.IFRAME,
},
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
it('should hard delete a page layout widget', async () => {
const { data: createData } = await createOnePageLayoutWidget({
expectToFail: false,
input: {
title: 'Widget To Destroy',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.IFRAME,
configuration: {
configurationType: WidgetConfigurationType.IFRAME,
},
});
gridPosition: {
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
},
});
const widgetId = createData.createPageLayoutWidget.id;
const widgetId = createData.createPageLayoutWidget.id;
if (operation === 'soft-delete-restore') {
const { data: deleteData } = await deleteOnePageLayoutWidget({
expectToFail: false,
input: { id: widgetId },
});
const { data: destroyData } = await destroyOnePageLayoutWidget({
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);
}
},
);
expect(destroyData.destroyPageLayoutWidget).toBe(true);
});
});
@@ -1,43 +0,0 @@
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<DeleteOnePageLayoutWidgetFactoryInput>) => ({
query: gql`
mutation DeletePageLayoutWidget($id: String!) {
deletePageLayoutWidget(id: $id) {
${gqlFields}
}
}
`,
variables: {
id: input.id,
},
});
@@ -1,44 +0,0 @@
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-widget/dtos/page-layout-widget.dto';
export const deleteOnePageLayoutWidget = async ({
input,
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<DeleteOnePageLayoutWidgetFactoryInput>): 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 };
};
@@ -1,43 +0,0 @@
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<RestoreOnePageLayoutWidgetFactoryInput>) => ({
query: gql`
mutation RestorePageLayoutWidget($id: String!) {
restorePageLayoutWidget(id: $id) {
${gqlFields}
}
}
`,
variables: {
id: input.id,
},
});
@@ -1,43 +0,0 @@
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-widget/dtos/page-layout-widget.dto';
export const restoreOnePageLayoutWidget = async ({
input,
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<RestoreOnePageLayoutWidgetFactoryInput>): 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 };
};
@@ -1,12 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Page layout deletion should fail when deleting a non-existent page layout 1`] = `
exports[`Page layout deletion should fail when destroying a non-existent page layout 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"userFriendlyMessage": "An error occurred.",
},
"message": "Page layout to delete not found",
"message": "Page layout to destroy not found",
"name": "NotFoundError",
}
`;
@@ -1,25 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Page layout deletion should succeed should soft delete and restore a page layout 1`] = `
{
"createdAt": Any<String>,
"deletedAt": Any<String>,
"id": Any<String>,
"name": "Page Layout To Delete",
"objectMetadataId": null,
"type": "RECORD_PAGE",
"updatedAt": Any<String>,
}
`;
exports[`Page layout deletion should succeed should soft delete and restore a page layout 2`] = `
{
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"name": "Page Layout To Delete",
"objectMetadataId": null,
"type": "RECORD_PAGE",
"updatedAt": Any<String>,
}
`;
@@ -1,10 +1,10 @@
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';
import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
describe('Page layout deletion should fail', () => {
it('when deleting a non-existent page layout', async () => {
const { errors } = await deleteOnePageLayout({
it('when destroying a non-existent page layout', async () => {
const { errors } = await destroyOnePageLayout({
expectToFail: true,
input: { id: faker.string.uuid() },
});
@@ -1,81 +1,20 @@
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<TestContext>[] = [
{
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 },
});
it('should hard delete a page layout', async () => {
const { data: createData } = await createOnePageLayout({
expectToFail: false,
input: { name: 'Page Layout To Destroy' },
});
const pageLayoutId = createData.createPageLayout.id;
const pageLayoutId = createData.createPageLayout.id;
if (operation === 'soft-delete-restore') {
const { data: deleteData } = await deleteOnePageLayout({
expectToFail: false,
input: { id: pageLayoutId },
});
const { data: destroyData } = await destroyOnePageLayout({
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);
}
},
);
expect(destroyData.destroyPageLayout).toBe(true);
});
});
@@ -1,32 +0,0 @@
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<DeleteOnePageLayoutFactoryInput>) => ({
query: gql`
mutation DeletePageLayout($id: String!) {
deletePageLayout(id: $id) {
${gqlFields}
}
}
`,
variables: {
id: input.id,
},
});
@@ -1,43 +0,0 @@
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<DeleteOnePageLayoutFactoryInput>): 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 };
};
@@ -1,32 +0,0 @@
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<RestoreOnePageLayoutFactoryInput>) => ({
query: gql`
mutation RestorePageLayout($id: String!) {
restorePageLayout(id: $id) {
${gqlFields}
}
}
`,
variables: {
id: input.id,
},
});
@@ -1,43 +0,0 @@
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<RestoreOnePageLayoutFactoryInput>): 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 };
};