[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
@@ -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 };
};