snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 3s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Page layout widget creation should fail AGGREGATE_CHART widget configuration validation failures when AGGREGATE_CHART configuration has invalid UUID 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "aggregateFieldMetadataId: aggregateFieldMetadataId must be a UUID",
"name": "UserInputError",
}
`;
exports[`Page layout widget creation should fail AGGREGATE_CHART widget configuration validation failures when AGGREGATE_CHART configuration has missing required fields 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "aggregateFieldMetadataId: aggregateFieldMetadataId should not be empty, aggregateFieldMetadataId must be a UUID; aggregateOperation: aggregateOperation should not be empty, aggregateOperation must be one of the following values: MIN, MAX, AVG, SUM, COUNT, COUNT_UNIQUE_VALUES, COUNT_EMPTY, COUNT_NOT_EMPTY, COUNT_TRUE, COUNT_FALSE, PERCENTAGE_EMPTY, PERCENTAGE_NOT_EMPTY",
"name": "UserInputError",
}
`;
exports[`Page layout widget creation should fail BAR_CHART widget configuration validation failures when HORIZONTAL BAR_CHART configuration is missing group by field 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "primaryAxisGroupByFieldMetadataId: primaryAxisGroupByFieldMetadataId should not be empty, primaryAxisGroupByFieldMetadataId must be a UUID; layout: layout should not be empty, layout must be one of the following values: VERTICAL, HORIZONTAL",
"name": "UserInputError",
}
`;
exports[`Page layout widget creation should fail BAR_CHART widget configuration validation failures when VERTICAL BAR_CHART configuration is missing group by field 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "primaryAxisGroupByFieldMetadataId: primaryAxisGroupByFieldMetadataId should not be empty, primaryAxisGroupByFieldMetadataId must be a UUID; layout: layout should not be empty, layout must be one of the following values: VERTICAL, HORIZONTAL",
"name": "UserInputError",
}
`;
exports[`Page layout widget creation should fail Edge case configuration validation failures when configuration has missing configurationType 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "Invalid configuration: missing configuration type",
"name": "UserInputError",
}
`;
exports[`Page layout widget creation should fail Edge case configuration validation failures when configuration has unsupported configurationType 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "Invalid configuration type: UNSUPPORTED_TYPE",
"name": "UserInputError",
}
`;
exports[`Page layout widget creation should fail Edge case configuration validation failures when configuration is null 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
},
"message": "Expected non-nullable type "JSON!" not to be null.",
"name": "GraphQLError",
}
`;
exports[`Page layout widget creation should fail General validation failures when pageLayoutTabId references non-existent tab 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"subCode": "RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND",
"userFriendlyMessage": "An error occurred.",
},
"message": "Could not find pageLayoutTab for given pageLayoutTabId",
"name": "NotFoundError",
}
`;
exports[`Page layout widget creation should fail General validation failures when title is missing 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
},
"message": "Field "title" of required type "String!" was not provided.",
"name": "GraphQLError",
}
`;
exports[`Page layout widget creation should fail IFRAME widget configuration validation failures when IFRAME configuration has empty URL 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "url: url must be a URL address",
"name": "UserInputError",
}
`;
exports[`Page layout widget creation should fail IFRAME widget configuration validation failures when IFRAME configuration has invalid URL 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "url: url must be a URL address",
"name": "UserInputError",
}
`;
exports[`Page layout widget creation should fail STANDALONE_RICH_TEXT widget configuration validation failures when STANDALONE_RICH_TEXT configuration has missing body 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "body: body should not be empty",
"name": "UserInputError",
}
`;
exports[`Page layout widget creation should fail STANDALONE_RICH_TEXT widget configuration validation failures when STANDALONE_RICH_TEXT configuration has wrong body type 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "body: nested property body must be either object or array",
"name": "UserInputError",
}
`;

View File

@@ -0,0 +1,133 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Page layout widget update should fail Widget configuration validation failures on update AGGREGATE_CHART widget configuration validation failures when updating to AGGREGATE_CHART with invalid UUID 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "aggregateFieldMetadataId: aggregateFieldMetadataId must be a UUID",
"name": "UserInputError",
}
`;
exports[`Page layout widget update should fail Widget configuration validation failures on update AGGREGATE_CHART widget configuration validation failures when updating to AGGREGATE_CHART with missing required fields 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "aggregateFieldMetadataId: aggregateFieldMetadataId should not be empty, aggregateFieldMetadataId must be a UUID; aggregateOperation: aggregateOperation should not be empty, aggregateOperation must be one of the following values: MIN, MAX, AVG, SUM, COUNT, COUNT_UNIQUE_VALUES, COUNT_EMPTY, COUNT_NOT_EMPTY, COUNT_TRUE, COUNT_FALSE, PERCENTAGE_EMPTY, PERCENTAGE_NOT_EMPTY",
"name": "UserInputError",
}
`;
exports[`Page layout widget update should fail Widget configuration validation failures on update BAR_CHART widget configuration validation failures when updating to BAR_CHART with missing group by field (horizontal) 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "primaryAxisGroupByFieldMetadataId: primaryAxisGroupByFieldMetadataId should not be empty, primaryAxisGroupByFieldMetadataId must be a UUID; layout: layout should not be empty, layout must be one of the following values: VERTICAL, HORIZONTAL",
"name": "UserInputError",
}
`;
exports[`Page layout widget update should fail Widget configuration validation failures on update BAR_CHART widget configuration validation failures when updating to BAR_CHART with missing group by field (vertical) 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "primaryAxisGroupByFieldMetadataId: primaryAxisGroupByFieldMetadataId should not be empty, primaryAxisGroupByFieldMetadataId must be a UUID; layout: layout should not be empty, layout must be one of the following values: VERTICAL, HORIZONTAL",
"name": "UserInputError",
}
`;
exports[`Page layout widget update should fail Widget configuration validation failures on update Edge case configuration validation failures when updating configuration to null 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "Invalid configuration: not an object",
"name": "UserInputError",
}
`;
exports[`Page layout widget update should fail Widget configuration validation failures on update Edge case configuration validation failures when updating configuration with missing configurationType 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "Invalid configuration: missing configuration type",
"name": "UserInputError",
}
`;
exports[`Page layout widget update should fail Widget configuration validation failures on update Edge case configuration validation failures when updating configuration with unsupported configurationType 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "Invalid configuration type: UNSUPPORTED_TYPE",
"name": "UserInputError",
}
`;
exports[`Page layout widget update should fail Widget configuration validation failures on update IFRAME widget configuration validation failures when updating IFRAME configuration with empty URL 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "url: url must be a URL address",
"name": "UserInputError",
}
`;
exports[`Page layout widget update should fail Widget configuration validation failures on update IFRAME widget configuration validation failures when updating IFRAME configuration with invalid URL 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "url: url must be a URL address",
"name": "UserInputError",
}
`;
exports[`Page layout widget update should fail Widget configuration validation failures on update STANDALONE_RICH_TEXT widget configuration validation failures when updating to STANDALONE_RICH_TEXT with missing body 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "body: body should not be empty",
"name": "UserInputError",
}
`;
exports[`Page layout widget update should fail Widget configuration validation failures on update STANDALONE_RICH_TEXT widget configuration validation failures when updating to STANDALONE_RICH_TEXT with wrong body type 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"userFriendlyMessage": "Invalid page layout widget data.",
},
"message": "body: nested property body must be either object or array",
"name": "UserInputError",
}
`;
exports[`Page layout widget update should fail when updating a non-existent page layout widget 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"userFriendlyMessage": "An error occurred.",
},
"message": "Page layout widget with ID "ed3752e3-db7f-42ff-82c0-2757a5410044" not found",
"name": "NotFoundError",
}
`;

View File

@@ -0,0 +1,547 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Page layout widget creation should succeed should create a page layout widget with AGGREGATE_CHART full configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "COUNT",
"configurationType": "AGGREGATE_CHART",
"description": "Count of all records",
"displayDataLabel": true,
"filter": null,
"firstDayOfTheWeek": 1,
"label": "Total Records",
"numberFormat": "SHORT",
"prefix": null,
"suffix": null,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Number Chart Widget",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with AGGREGATE_CHART minimal configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "SUM",
"configurationType": "AGGREGATE_CHART",
"description": null,
"displayDataLabel": false,
"filter": null,
"firstDayOfTheWeek": 1,
"label": null,
"numberFormat": null,
"prefix": null,
"suffix": null,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Number Chart Widget Minimal",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with HORIZONTAL BAR_CHART full configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "SUM",
"axisNameDisplay": "NONE",
"color": "blue",
"configurationType": "BAR_CHART",
"description": "Horizontal revenue breakdown",
"displayDataLabel": true,
"displayLegend": true,
"filter": null,
"firstDayOfTheWeek": 1,
"groupMode": null,
"isCumulative": null,
"layout": "HORIZONTAL",
"numberFormat": "SHORT",
"omitNullValues": true,
"primaryAxisDateGranularity": "DAY",
"primaryAxisGroupByFieldMetadataId": Any<String>,
"primaryAxisGroupBySubFieldName": null,
"primaryAxisManualSortOrder": null,
"primaryAxisOrderBy": "FIELD_ASC",
"rangeMax": 100000,
"rangeMin": 0,
"secondaryAxisGroupByDateGranularity": "DAY",
"secondaryAxisGroupByFieldMetadataId": null,
"secondaryAxisGroupBySubFieldName": null,
"secondaryAxisManualSortOrder": null,
"secondaryAxisOrderBy": null,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Horizontal Bar Chart Widget",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with HORIZONTAL BAR_CHART minimal configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "COUNT",
"axisNameDisplay": "NONE",
"color": null,
"configurationType": "BAR_CHART",
"description": null,
"displayDataLabel": false,
"displayLegend": true,
"filter": null,
"firstDayOfTheWeek": 1,
"groupMode": null,
"isCumulative": null,
"layout": "HORIZONTAL",
"numberFormat": null,
"omitNullValues": null,
"primaryAxisDateGranularity": "DAY",
"primaryAxisGroupByFieldMetadataId": Any<String>,
"primaryAxisGroupBySubFieldName": null,
"primaryAxisManualSortOrder": null,
"primaryAxisOrderBy": "VALUE_DESC",
"rangeMax": null,
"rangeMin": null,
"secondaryAxisGroupByDateGranularity": "DAY",
"secondaryAxisGroupByFieldMetadataId": null,
"secondaryAxisGroupBySubFieldName": null,
"secondaryAxisManualSortOrder": null,
"secondaryAxisOrderBy": null,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Horizontal Bar Chart Widget Minimal",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with IFRAME configuration 1`] = `
{
"configuration": {
"configurationType": "IFRAME",
"url": "https://example.com/dashboard",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": null,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Iframe Widget",
"type": "IFRAME",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with IFRAME minimal configuration 1`] = `
{
"configuration": {
"configurationType": "IFRAME",
"url": null,
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": null,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Iframe Widget Minimal",
"type": "IFRAME",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with LINE_CHART full configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "AVG",
"axisNameDisplay": "NONE",
"color": "cyan",
"configurationType": "LINE_CHART",
"description": "Trend over time",
"displayDataLabel": true,
"displayLegend": true,
"filter": null,
"firstDayOfTheWeek": 1,
"isCumulative": null,
"isStacked": null,
"numberFormat": "SHORT",
"omitNullValues": false,
"primaryAxisDateGranularity": "DAY",
"primaryAxisGroupByFieldMetadataId": Any<String>,
"primaryAxisGroupBySubFieldName": null,
"primaryAxisManualSortOrder": null,
"primaryAxisOrderBy": "FIELD_ASC",
"rangeMax": 100,
"rangeMin": -100,
"secondaryAxisGroupByDateGranularity": "DAY",
"secondaryAxisGroupByFieldMetadataId": Any<String>,
"secondaryAxisGroupBySubFieldName": "primaryLinkUrl",
"secondaryAxisManualSortOrder": null,
"secondaryAxisOrderBy": "FIELD_DESC",
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Line Chart Widget",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with LINE_CHART minimal configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "MAX",
"axisNameDisplay": "NONE",
"color": null,
"configurationType": "LINE_CHART",
"description": null,
"displayDataLabel": false,
"displayLegend": true,
"filter": null,
"firstDayOfTheWeek": 1,
"isCumulative": null,
"isStacked": null,
"numberFormat": null,
"omitNullValues": null,
"primaryAxisDateGranularity": "DAY",
"primaryAxisGroupByFieldMetadataId": Any<String>,
"primaryAxisGroupBySubFieldName": null,
"primaryAxisManualSortOrder": null,
"primaryAxisOrderBy": "VALUE_ASC",
"rangeMax": null,
"rangeMin": null,
"secondaryAxisGroupByDateGranularity": "DAY",
"secondaryAxisGroupByFieldMetadataId": null,
"secondaryAxisGroupBySubFieldName": null,
"secondaryAxisManualSortOrder": null,
"secondaryAxisOrderBy": null,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Line Chart Widget Minimal",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with PIE_CHART full configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "SUM",
"color": "yellow",
"configurationType": "PIE_CHART",
"dateGranularity": "DAY",
"description": "Distribution by category",
"displayDataLabel": true,
"displayLegend": true,
"filter": null,
"firstDayOfTheWeek": 1,
"groupByFieldMetadataId": Any<String>,
"groupBySubFieldName": null,
"manualSortOrder": null,
"numberFormat": "SHORT",
"orderBy": "VALUE_DESC",
"showCenterMetric": true,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Pie Chart Widget",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with PIE_CHART minimal configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "COUNT",
"color": null,
"configurationType": "PIE_CHART",
"dateGranularity": "DAY",
"description": null,
"displayDataLabel": false,
"displayLegend": true,
"filter": null,
"firstDayOfTheWeek": 1,
"groupByFieldMetadataId": Any<String>,
"groupBySubFieldName": null,
"manualSortOrder": null,
"numberFormat": null,
"orderBy": "FIELD_ASC",
"showCenterMetric": true,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Pie Chart Widget Minimal",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with STANDALONE_RICH_TEXT configuration 1`] = `
{
"configuration": {
"body": {
"blocknote": "{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Hello world"}]}]}",
"markdown": "# Hello world",
},
"configurationType": "STANDALONE_RICH_TEXT",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": null,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Rich Text Widget",
"type": "STANDALONE_RICH_TEXT",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with STANDALONE_RICH_TEXT minimal configuration 1`] = `
{
"configuration": {
"body": {
"blocknote": "{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Simple text"}]}]}",
"markdown": "{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Simple text"}]}]}",
},
"configurationType": "STANDALONE_RICH_TEXT",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": null,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Rich Text Widget Minimal",
"type": "STANDALONE_RICH_TEXT",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with VERTICAL BAR_CHART full configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "SUM",
"axisNameDisplay": "NONE",
"color": "red",
"configurationType": "BAR_CHART",
"description": "Monthly revenue breakdown",
"displayDataLabel": true,
"displayLegend": true,
"filter": null,
"firstDayOfTheWeek": 1,
"groupMode": null,
"isCumulative": null,
"layout": "VERTICAL",
"numberFormat": "SHORT",
"omitNullValues": true,
"primaryAxisDateGranularity": "DAY",
"primaryAxisGroupByFieldMetadataId": Any<String>,
"primaryAxisGroupBySubFieldName": null,
"primaryAxisManualSortOrder": null,
"primaryAxisOrderBy": "FIELD_ASC",
"rangeMax": 100000,
"rangeMin": 0,
"secondaryAxisGroupByDateGranularity": "DAY",
"secondaryAxisGroupByFieldMetadataId": null,
"secondaryAxisGroupBySubFieldName": null,
"secondaryAxisManualSortOrder": null,
"secondaryAxisOrderBy": null,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Vertical Bar Chart Widget",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget creation should succeed should create a page layout widget with VERTICAL BAR_CHART minimal configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "COUNT",
"axisNameDisplay": "NONE",
"color": null,
"configurationType": "BAR_CHART",
"description": null,
"displayDataLabel": false,
"displayLegend": true,
"filter": null,
"firstDayOfTheWeek": 1,
"groupMode": null,
"isCumulative": null,
"layout": "VERTICAL",
"numberFormat": null,
"omitNullValues": null,
"primaryAxisDateGranularity": "DAY",
"primaryAxisGroupByFieldMetadataId": Any<String>,
"primaryAxisGroupBySubFieldName": null,
"primaryAxisManualSortOrder": null,
"primaryAxisOrderBy": "VALUE_DESC",
"rangeMax": null,
"rangeMin": null,
"secondaryAxisGroupByDateGranularity": "DAY",
"secondaryAxisGroupByFieldMetadataId": null,
"secondaryAxisGroupBySubFieldName": null,
"secondaryAxisManualSortOrder": null,
"secondaryAxisOrderBy": null,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Vertical Bar Chart Widget Minimal",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;

View File

@@ -0,0 +1,322 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Page layout widget update should succeed from a GRAPH widget should update page layout widget to AGGREGATE_CHART configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "COUNT",
"configurationType": "AGGREGATE_CHART",
"description": "Count of all records",
"displayDataLabel": true,
"filter": null,
"firstDayOfTheWeek": 1,
"label": "Total Records",
"numberFormat": "SHORT",
"prefix": null,
"suffix": null,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Original Graph Widget",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget update should succeed from a GRAPH widget should update page layout widget to HORIZONTAL BAR_CHART configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "SUM",
"axisNameDisplay": "NONE",
"color": "blue",
"configurationType": "BAR_CHART",
"description": "Horizontal revenue breakdown",
"displayDataLabel": true,
"displayLegend": true,
"filter": null,
"firstDayOfTheWeek": 1,
"groupMode": null,
"isCumulative": null,
"layout": "HORIZONTAL",
"numberFormat": null,
"omitNullValues": true,
"primaryAxisDateGranularity": "DAY",
"primaryAxisGroupByFieldMetadataId": Any<String>,
"primaryAxisGroupBySubFieldName": null,
"primaryAxisManualSortOrder": null,
"primaryAxisOrderBy": "FIELD_ASC",
"rangeMax": 100000,
"rangeMin": 0,
"secondaryAxisGroupByDateGranularity": "DAY",
"secondaryAxisGroupByFieldMetadataId": null,
"secondaryAxisGroupBySubFieldName": null,
"secondaryAxisManualSortOrder": null,
"secondaryAxisOrderBy": null,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Original Graph Widget",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget update should succeed from a GRAPH widget should update page layout widget to LINE_CHART configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "AVG",
"axisNameDisplay": "NONE",
"color": "cyan",
"configurationType": "LINE_CHART",
"description": "Trend over time",
"displayDataLabel": true,
"displayLegend": true,
"filter": null,
"firstDayOfTheWeek": 1,
"isCumulative": null,
"isStacked": null,
"numberFormat": null,
"omitNullValues": false,
"primaryAxisDateGranularity": "DAY",
"primaryAxisGroupByFieldMetadataId": Any<String>,
"primaryAxisGroupBySubFieldName": null,
"primaryAxisManualSortOrder": null,
"primaryAxisOrderBy": "FIELD_ASC",
"rangeMax": 100,
"rangeMin": -100,
"secondaryAxisGroupByDateGranularity": "DAY",
"secondaryAxisGroupByFieldMetadataId": Any<String>,
"secondaryAxisGroupBySubFieldName": "primaryLinkUrl",
"secondaryAxisManualSortOrder": null,
"secondaryAxisOrderBy": "FIELD_DESC",
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Original Graph Widget",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget update should succeed from a GRAPH widget should update page layout widget to PIE_CHART configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "SUM",
"color": "yellow",
"configurationType": "PIE_CHART",
"dateGranularity": "DAY",
"description": "Distribution by category",
"displayDataLabel": true,
"displayLegend": true,
"filter": null,
"firstDayOfTheWeek": 1,
"groupByFieldMetadataId": Any<String>,
"groupBySubFieldName": null,
"manualSortOrder": null,
"numberFormat": null,
"orderBy": "VALUE_DESC",
"showCenterMetric": true,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Original Graph Widget",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget update should succeed from a GRAPH widget should update page layout widget to VERTICAL BAR_CHART configuration 1`] = `
{
"configuration": {
"aggregateFieldMetadataId": Any<String>,
"aggregateOperation": "SUM",
"axisNameDisplay": "NONE",
"color": "red",
"configurationType": "BAR_CHART",
"description": "Monthly revenue breakdown",
"displayDataLabel": true,
"displayLegend": true,
"filter": null,
"firstDayOfTheWeek": 1,
"groupMode": null,
"isCumulative": null,
"layout": "VERTICAL",
"numberFormat": null,
"omitNullValues": true,
"primaryAxisDateGranularity": "DAY",
"primaryAxisGroupByFieldMetadataId": Any<String>,
"primaryAxisGroupBySubFieldName": null,
"primaryAxisManualSortOrder": null,
"primaryAxisOrderBy": "FIELD_ASC",
"rangeMax": 100000,
"rangeMin": 0,
"secondaryAxisGroupByDateGranularity": "DAY",
"secondaryAxisGroupByFieldMetadataId": null,
"secondaryAxisGroupBySubFieldName": null,
"secondaryAxisManualSortOrder": null,
"secondaryAxisOrderBy": null,
"timezone": "UTC",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": Any<String>,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Original Graph Widget",
"type": "GRAPH",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget update should succeed from a STANDALONE_RICH_TEXT widget should update page layout widget to STANDALONE_RICH_TEXT configuration 1`] = `
{
"configuration": {
"body": {
"blocknote": "{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Hello world"}]}]}",
"markdown": "# Hello world",
},
"configurationType": "STANDALONE_RICH_TEXT",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": null,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Original Rich Text Widget",
"type": "STANDALONE_RICH_TEXT",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget update should succeed from an IFRAME widget should update page layout widget grid position 1`] = `
{
"configuration": {
"configurationType": "IFRAME",
"url": "https://example.com/dashboard",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": null,
"pageLayoutTabId": Any<String>,
"position": {
"column": 3,
"columnSpan": 4,
"layoutMode": "GRID",
"row": 2,
"rowSpan": 2,
},
"title": "Original Widget Title",
"type": "IFRAME",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget update should succeed from an IFRAME widget should update page layout widget title 1`] = `
{
"configuration": {
"configurationType": "IFRAME",
"url": "https://example.com/dashboard",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": null,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Updated Widget Title",
"type": "IFRAME",
"updatedAt": Any<String>,
}
`;
exports[`Page layout widget update should succeed from an IFRAME widget should update page layout widget to IFRAME configuration 1`] = `
{
"configuration": {
"configurationType": "IFRAME",
"url": "https://app.twenty.com/analytics",
},
"createdAt": Any<String>,
"deletedAt": null,
"id": Any<String>,
"objectMetadataId": null,
"pageLayoutTabId": Any<String>,
"position": {
"column": 0,
"columnSpan": 1,
"layoutMode": "GRID",
"row": 0,
"rowSpan": 1,
},
"title": "Original Widget Title",
"type": "IFRAME",
"updatedAt": Any<String>,
}
`;

View File

@@ -0,0 +1,105 @@
export const WIDGET_CONFIGURATION_GQL_FIELDS = `
... on BarChartConfiguration {
configurationType
aggregateFieldMetadataId
aggregateOperation
primaryAxisGroupByFieldMetadataId
primaryAxisGroupBySubFieldName
primaryAxisDateGranularity
primaryAxisOrderBy
primaryAxisManualSortOrder
secondaryAxisGroupByFieldMetadataId
secondaryAxisGroupBySubFieldName
secondaryAxisGroupByDateGranularity
secondaryAxisOrderBy
secondaryAxisManualSortOrder
omitNullValues
axisNameDisplay
displayDataLabel
displayLegend
numberFormat
rangeMin
rangeMax
color
description
filter
groupMode
layout
isCumulative
timezone
firstDayOfTheWeek
}
... on LineChartConfiguration {
configurationType
aggregateFieldMetadataId
aggregateOperation
primaryAxisGroupByFieldMetadataId
primaryAxisGroupBySubFieldName
primaryAxisDateGranularity
primaryAxisOrderBy
primaryAxisManualSortOrder
secondaryAxisGroupByFieldMetadataId
secondaryAxisGroupBySubFieldName
secondaryAxisGroupByDateGranularity
secondaryAxisOrderBy
secondaryAxisManualSortOrder
omitNullValues
axisNameDisplay
displayDataLabel
displayLegend
numberFormat
rangeMin
rangeMax
color
description
filter
isStacked
isCumulative
timezone
firstDayOfTheWeek
}
... on PieChartConfiguration {
configurationType
groupByFieldMetadataId
aggregateFieldMetadataId
aggregateOperation
groupBySubFieldName
dateGranularity
orderBy
manualSortOrder
displayDataLabel
showCenterMetric
displayLegend
numberFormat
color
description
filter
timezone
firstDayOfTheWeek
}
... on AggregateChartConfiguration {
configurationType
aggregateFieldMetadataId
aggregateOperation
label
displayDataLabel
numberFormat
description
filter
prefix
suffix
timezone
firstDayOfTheWeek
}
... on IframeConfiguration {
configurationType
url
}
... on StandaloneRichTextConfiguration {
configurationType
body {
blocknote
markdown
}
}
`;

View File

@@ -0,0 +1,17 @@
export const WIDGET_POSITION_GQL_FIELDS = `
... on PageLayoutWidgetGridPosition {
layoutMode
row
column
rowSpan
columnSpan
}
... on PageLayoutWidgetVerticalListPosition {
layoutMode
index
heightBehavior
}
... on PageLayoutWidgetCanvasPosition {
layoutMode
}
`;

View File

@@ -0,0 +1,332 @@
import { faker } from '@faker-js/faker';
import {
INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_IFRAME_CONFIG_BAD_URL,
INVALID_IFRAME_CONFIG_EMPTY_URL,
INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
TEST_IFRAME_CONFIG,
} from 'test/integration/constants/widget-configuration-test-data.constants';
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util';
import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util';
import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util';
import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util';
import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
import { type CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { PageLayoutTabLayoutMode, WidgetType } from 'twenty-shared/types';
const DEFAULT_GRID_POSITION = {
layoutMode: PageLayoutTabLayoutMode.GRID as const,
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
};
describe('Page layout widget creation should fail', () => {
let testPageLayoutId: string;
let testPageLayoutTabId: string;
beforeAll(async () => {
const { data: layoutData } = await createOnePageLayout({
expectToFail: false,
input: { name: 'Test Page Layout For Widget Creation Failures' },
});
testPageLayoutId = layoutData.createPageLayout.id;
const { data: tabData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: 'Test Tab For Widget Creation Failures',
pageLayoutId: testPageLayoutId,
},
});
testPageLayoutTabId = tabData.createPageLayoutTab.id;
});
afterAll(async () => {
await destroyOnePageLayoutTab({
expectToFail: false,
input: { id: testPageLayoutTabId },
});
await destroyOnePageLayout({
expectToFail: false,
input: { id: testPageLayoutId },
});
});
describe('General validation failures', () => {
it('when title is missing', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.IFRAME,
position: DEFAULT_GRID_POSITION,
configuration: TEST_IFRAME_CONFIG,
} as CreatePageLayoutWidgetInput,
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when pageLayoutTabId references non-existent tab', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Widget With Non-Existent Tab',
pageLayoutTabId: faker.string.uuid(),
type: WidgetType.IFRAME,
configuration: TEST_IFRAME_CONFIG,
position: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when position has invalid values', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Widget With Invalid Grid Position',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.IFRAME,
configuration: TEST_IFRAME_CONFIG,
position: {
layoutMode: PageLayoutTabLayoutMode.GRID,
row: -1,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
},
});
expect(errors).toMatchObject([
{
extensions: {
errors: {
pageLayoutWidget: [
{
errors: [
{
message: expect.stringContaining(
'row must be an integer greater than or equal to 0',
),
},
],
},
],
},
},
},
]);
});
});
describe('IFRAME widget configuration validation failures', () => {
it('when IFRAME configuration has invalid URL', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Widget With Invalid URL',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.IFRAME,
configuration: {
...INVALID_IFRAME_CONFIG_BAD_URL,
configurationType: WidgetConfigurationType.IFRAME,
},
position: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when IFRAME configuration has empty URL', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Widget With Empty URL',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.IFRAME,
configuration: {
...INVALID_IFRAME_CONFIG_EMPTY_URL,
configurationType: WidgetConfigurationType.IFRAME,
},
position: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
});
describe('STANDALONE_RICH_TEXT widget configuration validation failures', () => {
it('when STANDALONE_RICH_TEXT configuration has missing body', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Widget With Missing Body',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.STANDALONE_RICH_TEXT,
configuration: {
...INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
} as unknown as CreatePageLayoutWidgetInput['configuration'],
position: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when STANDALONE_RICH_TEXT configuration has wrong body type', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Widget With Wrong Body Type',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.STANDALONE_RICH_TEXT,
configuration: {
...INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
} as unknown as CreatePageLayoutWidgetInput['configuration'],
position: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
});
describe('AGGREGATE_CHART widget configuration validation failures', () => {
it('when AGGREGATE_CHART configuration has missing required fields', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Widget With Missing Fields',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.GRAPH,
configuration:
INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS as unknown as CreatePageLayoutWidgetInput['configuration'],
position: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when AGGREGATE_CHART configuration has invalid UUID', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Widget With Bad UUID',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.GRAPH,
configuration:
INVALID_NUMBER_CHART_CONFIG_BAD_UUID as unknown as CreatePageLayoutWidgetInput['configuration'],
position: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
});
describe('BAR_CHART widget configuration validation failures', () => {
it('when VERTICAL BAR_CHART configuration is missing group by field', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Vertical Bar Chart Missing Group By',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.GRAPH,
configuration:
INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY as unknown as CreatePageLayoutWidgetInput['configuration'],
position: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when HORIZONTAL BAR_CHART configuration is missing group by field', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Horizontal Bar Chart Missing Group By',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.GRAPH,
configuration:
INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY as unknown as CreatePageLayoutWidgetInput['configuration'],
position: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
});
describe('Edge case configuration validation failures', () => {
it('when configuration is null', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Widget With Null Config',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.IFRAME,
configuration:
null as unknown as CreatePageLayoutWidgetInput['configuration'],
position: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when configuration has missing configurationType', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Widget With Missing Config Type',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.IFRAME,
configuration: {
someField: 'value',
} as unknown as CreatePageLayoutWidgetInput['configuration'],
position: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when configuration has unsupported configurationType', async () => {
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input: {
title: 'Widget With Unsupported Config Type',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.IFRAME,
configuration: {
configurationType: 'UNSUPPORTED_TYPE',
someField: 'value',
} as unknown as CreatePageLayoutWidgetInput['configuration'],
position: DEFAULT_GRID_POSITION,
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
});
});

View File

@@ -0,0 +1,267 @@
import {
INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_IFRAME_CONFIG_BAD_URL,
INVALID_IFRAME_CONFIG_EMPTY_URL,
INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
TEST_IFRAME_CONFIG,
} from 'test/integration/constants/widget-configuration-test-data.constants';
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util';
import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util';
import { createOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget.util';
import { destroyOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget.util';
import { updateOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget.util';
import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util';
import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
import { type UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { PageLayoutTabLayoutMode, WidgetType } from 'twenty-shared/types';
const DEFAULT_GRID_POSITION = {
layoutMode: PageLayoutTabLayoutMode.GRID as const,
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
};
describe('Page layout widget update should fail', () => {
it('when updating a non-existent page layout widget', async () => {
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: 'ed3752e3-db7f-42ff-82c0-2757a5410044',
title: 'Updated Title',
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
describe('Widget configuration validation failures on update', () => {
let testPageLayoutId: string;
let testPageLayoutTabId: string;
let testPageLayoutWidgetId: string;
beforeAll(async () => {
const { data: layoutData } = await createOnePageLayout({
expectToFail: false,
input: { name: 'Test Page Layout For Widget Update Failures' },
});
testPageLayoutId = layoutData.createPageLayout.id;
const { data: tabData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: 'Test Tab For Widget Update Failures',
pageLayoutId: testPageLayoutId,
},
});
testPageLayoutTabId = tabData.createPageLayoutTab.id;
});
afterAll(async () => {
await destroyOnePageLayoutTab({
expectToFail: false,
input: { id: testPageLayoutTabId },
});
await destroyOnePageLayout({
expectToFail: false,
input: { id: testPageLayoutId },
});
});
beforeEach(async () => {
const { data } = await createOnePageLayoutWidget({
expectToFail: false,
input: {
title: 'Original Widget',
pageLayoutTabId: testPageLayoutTabId,
type: WidgetType.IFRAME,
configuration: TEST_IFRAME_CONFIG,
position: DEFAULT_GRID_POSITION,
},
});
testPageLayoutWidgetId = data.createPageLayoutWidget.id;
});
afterEach(async () => {
await destroyOnePageLayoutWidget({
expectToFail: false,
input: { id: testPageLayoutWidgetId },
});
});
describe('IFRAME widget configuration validation failures', () => {
it('when updating IFRAME configuration with invalid URL', async () => {
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: testPageLayoutWidgetId,
configuration: {
...INVALID_IFRAME_CONFIG_BAD_URL,
configurationType: WidgetConfigurationType.IFRAME,
},
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when updating IFRAME configuration with empty URL', async () => {
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: testPageLayoutWidgetId,
configuration: {
...INVALID_IFRAME_CONFIG_EMPTY_URL,
configurationType: WidgetConfigurationType.IFRAME,
},
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
});
describe('STANDALONE_RICH_TEXT widget configuration validation failures', () => {
it('when updating to STANDALONE_RICH_TEXT with missing body', async () => {
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: testPageLayoutWidgetId,
configuration: {
...INVALID_STANDALONE_RICH_TEXT_CONFIG_MISSING_BODY,
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
} as unknown as UpdatePageLayoutWidgetInput['configuration'],
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when updating to STANDALONE_RICH_TEXT with wrong body type', async () => {
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: testPageLayoutWidgetId,
configuration: {
...INVALID_STANDALONE_RICH_TEXT_CONFIG_BODY_WRONG_TYPE,
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
} as unknown as UpdatePageLayoutWidgetInput['configuration'],
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
});
describe('AGGREGATE_CHART widget configuration validation failures', () => {
it('when updating to AGGREGATE_CHART with missing required fields', async () => {
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: testPageLayoutWidgetId,
configuration:
INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS as unknown as UpdatePageLayoutWidgetInput['configuration'],
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when updating to AGGREGATE_CHART with invalid UUID', async () => {
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: testPageLayoutWidgetId,
configuration:
INVALID_NUMBER_CHART_CONFIG_BAD_UUID as unknown as UpdatePageLayoutWidgetInput['configuration'],
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
});
describe('BAR_CHART widget configuration validation failures', () => {
it('when updating to BAR_CHART with missing group by field (vertical)', async () => {
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: testPageLayoutWidgetId,
configuration:
INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY as unknown as UpdatePageLayoutWidgetInput['configuration'],
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when updating to BAR_CHART with missing group by field (horizontal)', async () => {
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: testPageLayoutWidgetId,
configuration:
INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY as unknown as UpdatePageLayoutWidgetInput['configuration'],
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
});
describe('Edge case configuration validation failures', () => {
it('when updating configuration to null', async () => {
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: testPageLayoutWidgetId,
configuration:
null as unknown as UpdatePageLayoutWidgetInput['configuration'],
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when updating configuration with missing configurationType', async () => {
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: testPageLayoutWidgetId,
configuration: {
someField: 'value',
} as unknown as UpdatePageLayoutWidgetInput['configuration'],
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
it('when updating configuration with unsupported configurationType', async () => {
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: testPageLayoutWidgetId,
configuration: {
configurationType: 'UNSUPPORTED_TYPE',
someField: 'value',
} as unknown as UpdatePageLayoutWidgetInput['configuration'],
},
});
expectOneNotInternalServerErrorSnapshot({ errors });
});
});
});
});

View File

@@ -0,0 +1,255 @@
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 { findPageLayoutTabs } from 'test/integration/metadata/suites/page-layout-tab/utils/find-page-layout-tabs.util';
import { findPageLayoutWidgets } from 'test/integration/metadata/suites/page-layout-widget/utils/find-page-layout-widgets.util';
import { updateOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget.util';
import { findPageLayouts } from 'test/integration/metadata/suites/page-layout/utils/find-page-layouts.util';
import { PageLayoutTabLayoutMode, PageLayoutType } from 'twenty-shared/types';
const WIDGET_OVERRIDE_GQL_FIELDS = `
id
title
type
pageLayoutTabId
conditionalDisplay
createdAt
updatedAt
deletedAt
`;
describe('Page layout widget override behavior', () => {
let seededPageLayoutId: string;
let seededWidgetId: string;
let seededWidgetOriginalPageLayoutTabId: string;
let seededWidgetOriginalTabLayoutMode: PageLayoutTabLayoutMode;
let seededWidgetOriginalTitle: string;
beforeAll(async () => {
const { data: layoutsData } = await findPageLayouts({
expectToFail: false,
input: undefined,
});
const recordPageLayout = layoutsData.getPageLayouts.find(
(layout) => layout.type === PageLayoutType.RECORD_PAGE,
);
expect(recordPageLayout).toBeDefined();
seededPageLayoutId = recordPageLayout!.id;
const { data: tabsData } = await findPageLayoutTabs({
expectToFail: false,
input: { pageLayoutId: seededPageLayoutId },
gqlFields: `
id
title
position
layoutMode
pageLayoutId
`,
});
expect(tabsData.getPageLayoutTabs.length).toBeGreaterThanOrEqual(1);
const firstTabId = tabsData.getPageLayoutTabs[0].id;
seededWidgetOriginalTabLayoutMode = tabsData.getPageLayoutTabs[0]
.layoutMode as PageLayoutTabLayoutMode;
const { data: widgetsData } = await findPageLayoutWidgets({
expectToFail: false,
input: { pageLayoutTabId: firstTabId },
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
expect(widgetsData.getPageLayoutWidgets.length).toBeGreaterThanOrEqual(1);
const firstWidget = widgetsData.getPageLayoutWidgets[0];
seededWidgetId = firstWidget.id;
seededWidgetOriginalPageLayoutTabId = firstWidget.pageLayoutTabId;
seededWidgetOriginalTitle = firstWidget.title;
});
afterAll(async () => {
await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: seededWidgetId,
pageLayoutTabId: seededWidgetOriginalPageLayoutTabId,
title: seededWidgetOriginalTitle,
},
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
});
it('should override a seeded widget title and return the overridden value', async () => {
const overriddenTitle = `Widget Override ${Date.now()}`;
const { data } = await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: seededWidgetId,
title: overriddenTitle,
},
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
expect(data.updatePageLayoutWidget.title).toBe(overriddenTitle);
});
it('should move a seeded widget to another tab through an override', async () => {
let destinationTabId: string | undefined;
try {
const { data: tabData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: `Widget Move Override ${Date.now()}`,
pageLayoutId: seededPageLayoutId,
layoutMode: seededWidgetOriginalTabLayoutMode,
},
});
destinationTabId = tabData.createPageLayoutTab.id;
const { data } = await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: seededWidgetId,
pageLayoutTabId: destinationTabId,
},
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
expect(data.updatePageLayoutWidget.pageLayoutTabId).toBe(
destinationTabId,
);
const { data: originalTabWidgetsData } = await findPageLayoutWidgets({
expectToFail: false,
input: { pageLayoutTabId: seededWidgetOriginalPageLayoutTabId },
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
expect(
originalTabWidgetsData.getPageLayoutWidgets.map((widget) => widget.id),
).not.toContain(seededWidgetId);
const { data: destinationTabWidgetsData } = await findPageLayoutWidgets({
expectToFail: false,
input: { pageLayoutTabId: destinationTabId },
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
expect(
destinationTabWidgetsData.getPageLayoutWidgets.map(
(widget) => widget.id,
),
).toContain(seededWidgetId);
} finally {
await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: seededWidgetId,
pageLayoutTabId: seededWidgetOriginalPageLayoutTabId,
},
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
if (destinationTabId !== undefined) {
await destroyOnePageLayoutTab({
expectToFail: false,
input: { id: destinationTabId },
});
}
}
});
it('should return the overridden title when querying the widget', async () => {
const { data: tabsData } = await findPageLayoutTabs({
expectToFail: false,
input: {
pageLayoutId: (
await findPageLayouts({
expectToFail: false,
input: undefined,
})
).data.getPageLayouts.find(
(layout) => layout.type === PageLayoutType.RECORD_PAGE,
)!.id,
},
});
const { data: widgetsData } = await findPageLayoutWidgets({
expectToFail: false,
input: { pageLayoutTabId: tabsData.getPageLayoutTabs[0].id },
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
const widget = widgetsData.getPageLayoutWidgets.find(
(widgetItem) => widgetItem.id === seededWidgetId,
);
expect(widget).toBeDefined();
expect(widget!.title).not.toBe(seededWidgetOriginalTitle);
});
it('should implicitly restore when updating title back to original value', async () => {
const { data } = await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: seededWidgetId,
title: seededWidgetOriginalTitle,
},
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
expect(data.updatePageLayoutWidget.title).toBe(seededWidgetOriginalTitle);
});
it('should override conditionalDisplay independently from title', async () => {
const overriddenTitle = `Multi Override ${Date.now()}`;
const overriddenConditionalDisplay = {
fieldMetadataId: '00000000-0000-0000-0000-000000000001',
operator: 'is',
value: 'test',
};
const { data: titleData } = await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: seededWidgetId,
title: overriddenTitle,
},
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
expect(titleData.updatePageLayoutWidget.title).toBe(overriddenTitle);
const { data: conditionalData } = await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: seededWidgetId,
conditionalDisplay: overriddenConditionalDisplay,
},
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
expect(conditionalData.updatePageLayoutWidget.title).toBe(overriddenTitle);
expect(conditionalData.updatePageLayoutWidget.conditionalDisplay).toEqual(
overriddenConditionalDisplay,
);
await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: seededWidgetId,
title: seededWidgetOriginalTitle,
conditionalDisplay: null,
},
gqlFields: WIDGET_OVERRIDE_GQL_FIELDS,
});
});
});

View File

@@ -0,0 +1,389 @@
import {
TEST_IFRAME_CONFIG,
TEST_STANDALONE_RICH_TEXT_CONFIG,
TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL,
} from 'test/integration/constants/widget-configuration-test-data.constants';
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 { fetchTestFieldMetadataIds } from 'test/integration/metadata/suites/page-layout-widget/utils/fetch-test-field-metadata-ids.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 {
AggregateOperations,
PageLayoutTabLayoutMode,
type PageLayoutWidgetGridPosition,
WidgetType,
} from 'twenty-shared/types';
import { AxisNameDisplay } from 'src/engine/metadata-modules/page-layout-widget/enums/axis-name-display.enum';
import { BarChartLayout } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-layout.enum';
import { ChartNumberFormat } from 'src/engine/metadata-modules/page-layout-widget/enums/chart-number-format.enum';
import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-order-by.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
type StaticTestContext = {
input: {
title: string;
type: WidgetType;
configuration: AllPageLayoutWidgetConfiguration;
position: PageLayoutWidgetGridPosition;
};
};
type GraphTestContext = {
widgetTitle: string;
buildConfiguration: () => AllPageLayoutWidgetConfiguration;
};
const DEFAULT_GRID_POSITION = {
layoutMode: PageLayoutTabLayoutMode.GRID as const,
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
};
const STATIC_TEST_CASES: EachTestingContext<StaticTestContext>[] = [
{
title: 'create a page layout widget with IFRAME configuration',
context: {
input: {
title: 'Iframe Widget',
type: WidgetType.IFRAME,
configuration: TEST_IFRAME_CONFIG,
position: DEFAULT_GRID_POSITION,
},
},
},
{
title: 'create a page layout widget with IFRAME minimal configuration',
context: {
input: {
title: 'Iframe Widget Minimal',
type: WidgetType.IFRAME,
configuration: {
configurationType: WidgetConfigurationType.IFRAME,
},
position: DEFAULT_GRID_POSITION,
},
},
},
{
title:
'create a page layout widget with STANDALONE_RICH_TEXT configuration',
context: {
input: {
title: 'Rich Text Widget',
type: WidgetType.STANDALONE_RICH_TEXT,
configuration: TEST_STANDALONE_RICH_TEXT_CONFIG,
position: DEFAULT_GRID_POSITION,
},
},
},
{
title:
'create a page layout widget with STANDALONE_RICH_TEXT minimal configuration',
context: {
input: {
title: 'Rich Text Widget Minimal',
type: WidgetType.STANDALONE_RICH_TEXT,
configuration: TEST_STANDALONE_RICH_TEXT_CONFIG_MINIMAL,
position: DEFAULT_GRID_POSITION,
},
},
},
];
describe('Page layout widget creation should succeed', () => {
let testSetup: {
pageLayoutId: string;
pageLayoutTabId: string;
objectMetadataId: string;
fieldMetadataId1: string;
fieldMetadataId2: string;
fieldMetadataId3: string;
fieldMetadataId3SubFieldName: string;
};
let createdPageLayoutWidgetId: string | undefined;
const graphTestCases: EachTestingContext<GraphTestContext>[] = [
{
title:
'create a page layout widget with AGGREGATE_CHART full configuration',
context: {
widgetTitle: 'Number Chart Widget',
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.AGGREGATE_CHART,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.COUNT,
label: 'Total Records',
description: 'Count of all records',
numberFormat: ChartNumberFormat.SHORT,
displayDataLabel: true,
}),
},
},
{
title:
'create a page layout widget with AGGREGATE_CHART minimal configuration',
context: {
widgetTitle: 'Number Chart Widget Minimal',
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.AGGREGATE_CHART,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.SUM,
displayDataLabel: false,
}),
},
},
{
title:
'create a page layout widget with VERTICAL BAR_CHART full configuration',
context: {
widgetTitle: 'Vertical Bar Chart Widget',
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.BAR_CHART,
layout: BarChartLayout.VERTICAL,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.SUM,
primaryAxisGroupByFieldMetadataId: testSetup.fieldMetadataId2,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
displayDataLabel: true,
numberFormat: ChartNumberFormat.SHORT,
axisNameDisplay: AxisNameDisplay.NONE,
color: 'red',
description: 'Monthly revenue breakdown',
omitNullValues: true,
rangeMin: 0,
rangeMax: 100000,
}),
},
},
{
title:
'create a page layout widget with VERTICAL BAR_CHART minimal configuration',
context: {
widgetTitle: 'Vertical Bar Chart Widget Minimal',
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.BAR_CHART,
layout: BarChartLayout.VERTICAL,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.COUNT,
primaryAxisGroupByFieldMetadataId: testSetup.fieldMetadataId2,
primaryAxisOrderBy: GraphOrderBy.VALUE_DESC,
displayDataLabel: false,
axisNameDisplay: AxisNameDisplay.NONE,
}),
},
},
{
title:
'create a page layout widget with HORIZONTAL BAR_CHART full configuration',
context: {
widgetTitle: 'Horizontal Bar Chart Widget',
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.BAR_CHART,
layout: BarChartLayout.HORIZONTAL,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.SUM,
primaryAxisGroupByFieldMetadataId: testSetup.fieldMetadataId2,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
displayDataLabel: true,
numberFormat: ChartNumberFormat.SHORT,
axisNameDisplay: AxisNameDisplay.NONE,
color: 'blue',
description: 'Horizontal revenue breakdown',
omitNullValues: true,
rangeMin: 0,
rangeMax: 100000,
}),
},
},
{
title:
'create a page layout widget with HORIZONTAL BAR_CHART minimal configuration',
context: {
widgetTitle: 'Horizontal Bar Chart Widget Minimal',
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.BAR_CHART,
layout: BarChartLayout.HORIZONTAL,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.COUNT,
primaryAxisGroupByFieldMetadataId: testSetup.fieldMetadataId2,
primaryAxisOrderBy: GraphOrderBy.VALUE_DESC,
displayDataLabel: false,
axisNameDisplay: AxisNameDisplay.NONE,
}),
},
},
{
title: 'create a page layout widget with PIE_CHART full configuration',
context: {
widgetTitle: 'Pie Chart Widget',
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.PIE_CHART,
groupByFieldMetadataId: testSetup.fieldMetadataId1,
aggregateFieldMetadataId: testSetup.fieldMetadataId2,
aggregateOperation: AggregateOperations.SUM,
orderBy: GraphOrderBy.VALUE_DESC,
displayDataLabel: true,
displayLegend: true,
numberFormat: ChartNumberFormat.SHORT,
showCenterMetric: true,
color: 'yellow',
description: 'Distribution by category',
}),
},
},
{
title: 'create a page layout widget with PIE_CHART minimal configuration',
context: {
widgetTitle: 'Pie Chart Widget Minimal',
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.PIE_CHART,
groupByFieldMetadataId: testSetup.fieldMetadataId1,
aggregateFieldMetadataId: testSetup.fieldMetadataId2,
aggregateOperation: AggregateOperations.COUNT,
orderBy: GraphOrderBy.FIELD_ASC,
displayDataLabel: false,
}),
},
},
{
title: 'create a page layout widget with LINE_CHART full configuration',
context: {
widgetTitle: 'Line Chart Widget',
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.LINE_CHART,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.AVG,
primaryAxisGroupByFieldMetadataId: testSetup.fieldMetadataId2,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
secondaryAxisGroupByFieldMetadataId: testSetup.fieldMetadataId3,
secondaryAxisGroupBySubFieldName:
testSetup.fieldMetadataId3SubFieldName,
secondaryAxisOrderBy: GraphOrderBy.FIELD_DESC,
displayDataLabel: true,
numberFormat: ChartNumberFormat.SHORT,
axisNameDisplay: AxisNameDisplay.NONE,
color: 'cyan',
description: 'Trend over time',
omitNullValues: false,
rangeMin: -100,
rangeMax: 100,
}),
},
},
{
title:
'create a page layout widget with LINE_CHART minimal configuration',
context: {
widgetTitle: 'Line Chart Widget Minimal',
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.LINE_CHART,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.MAX,
primaryAxisGroupByFieldMetadataId: testSetup.fieldMetadataId2,
primaryAxisOrderBy: GraphOrderBy.VALUE_ASC,
displayDataLabel: false,
axisNameDisplay: AxisNameDisplay.NONE,
}),
},
},
];
beforeAll(async () => {
const testFieldMetadataIds = await fetchTestFieldMetadataIds();
const { data: layoutData } = await createOnePageLayout({
expectToFail: false,
input: { name: 'Test Page Layout For Widgets' },
});
const { data: tabData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: 'Test Tab For Widgets',
pageLayoutId: layoutData.createPageLayout.id,
},
});
testSetup = {
pageLayoutId: layoutData.createPageLayout.id,
pageLayoutTabId: tabData.createPageLayoutTab.id,
...testFieldMetadataIds,
};
});
afterAll(async () => {
await destroyOnePageLayoutTab({
expectToFail: false,
input: { id: testSetup.pageLayoutTabId },
});
await destroyOnePageLayout({
expectToFail: false,
input: { id: testSetup.pageLayoutId },
});
});
afterEach(async () => {
if (createdPageLayoutWidgetId) {
await destroyOnePageLayoutWidget({
expectToFail: false,
input: { id: createdPageLayoutWidgetId },
});
}
createdPageLayoutWidgetId = undefined;
});
it.each(eachTestingContextFilter(STATIC_TEST_CASES))(
'should $title',
async ({ context: { input } }) => {
const { data } = await createOnePageLayoutWidget({
expectToFail: false,
input: {
...input,
pageLayoutTabId: testSetup.pageLayoutTabId,
},
});
createdPageLayoutWidgetId = data?.createPageLayoutWidget?.id;
expect(data.createPageLayoutWidget).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny({ ...data.createPageLayoutWidget }),
);
},
);
it.each(eachTestingContextFilter(graphTestCases))(
'should $title',
async ({ context: { widgetTitle, buildConfiguration } }) => {
const { data } = await createOnePageLayoutWidget({
expectToFail: false,
input: {
title: widgetTitle,
type: WidgetType.GRAPH,
objectMetadataId: testSetup.objectMetadataId,
configuration: buildConfiguration(),
pageLayoutTabId: testSetup.pageLayoutTabId,
position: DEFAULT_GRID_POSITION,
},
});
createdPageLayoutWidgetId = data?.createPageLayoutWidget?.id;
expect(data.createPageLayoutWidget).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny({ ...data.createPageLayoutWidget }),
);
},
);
});

View File

@@ -0,0 +1,74 @@
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 { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { PageLayoutTabLayoutMode, WidgetType } from 'twenty-shared/types';
describe('Page layout widget deletion should succeed', () => {
let testPageLayoutId: string;
let testPageLayoutTabId: string;
beforeAll(async () => {
const { data: layoutData } = await createOnePageLayout({
expectToFail: false,
input: { name: 'Test Page Layout For Widget Deletions' },
});
testPageLayoutId = layoutData.createPageLayout.id;
const { data: tabData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: 'Test Tab For Widget Deletions',
pageLayoutId: testPageLayoutId,
},
});
testPageLayoutTabId = tabData.createPageLayoutTab.id;
});
afterAll(async () => {
await destroyOnePageLayoutTab({
expectToFail: false,
input: { id: testPageLayoutTabId },
});
await destroyOnePageLayout({
expectToFail: false,
input: { id: testPageLayoutId },
});
});
it('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,
},
position: {
layoutMode: PageLayoutTabLayoutMode.GRID,
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
},
});
const widgetId = createData.createPageLayoutWidget.id;
const { data: destroyData } = await destroyOnePageLayoutWidget({
expectToFail: false,
input: { id: widgetId },
});
expect(destroyData.destroyPageLayoutWidget).toBe(true);
});
});

View File

@@ -0,0 +1,370 @@
import {
TEST_IFRAME_CONFIG,
TEST_IFRAME_CONFIG_ALTERNATIVE,
TEST_STANDALONE_RICH_TEXT_CONFIG,
} from 'test/integration/constants/widget-configuration-test-data.constants';
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 { fetchTestFieldMetadataIds } from 'test/integration/metadata/suites/page-layout-widget/utils/fetch-test-field-metadata-ids.util';
import { updateOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget.util';
import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util';
import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import {
AggregateOperations,
PageLayoutTabLayoutMode,
type PageLayoutWidgetGridPosition,
WidgetType,
} from 'twenty-shared/types';
import { AxisNameDisplay } from 'src/engine/metadata-modules/page-layout-widget/enums/axis-name-display.enum';
import { BarChartLayout } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-layout.enum';
import { ChartNumberFormat } from 'src/engine/metadata-modules/page-layout-widget/enums/chart-number-format.enum';
import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-order-by.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
type StaticUpdateTestContext = {
input: {
title?: string;
configuration?: AllPageLayoutWidgetConfiguration;
position?: PageLayoutWidgetGridPosition;
};
};
type GraphUpdateTestContext = {
buildConfiguration: () => AllPageLayoutWidgetConfiguration;
};
const DEFAULT_GRID_POSITION = {
layoutMode: PageLayoutTabLayoutMode.GRID as const,
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
};
const IFRAME_UPDATE_TEST_CASES: EachTestingContext<StaticUpdateTestContext>[] =
[
{
title: 'update page layout widget title',
context: {
input: {
title: 'Updated Widget Title',
},
},
},
{
title: 'update page layout widget grid position',
context: {
input: {
position: {
layoutMode: PageLayoutTabLayoutMode.GRID,
row: 2,
column: 3,
rowSpan: 2,
columnSpan: 4,
},
},
},
},
{
title: 'update page layout widget to IFRAME configuration',
context: {
input: {
configuration: TEST_IFRAME_CONFIG_ALTERNATIVE,
},
},
},
];
describe('Page layout widget update should succeed', () => {
let testSetup: {
pageLayoutId: string;
pageLayoutTabId: string;
objectMetadataId: string;
fieldMetadataId1: string;
fieldMetadataId2: string;
fieldMetadataId3: string;
fieldMetadataId3SubFieldName: string;
};
beforeAll(async () => {
const testFieldMetadataIds = await fetchTestFieldMetadataIds();
const { data: layoutData } = await createOnePageLayout({
expectToFail: false,
input: { name: 'Test Page Layout For Widget Updates' },
});
const { data: tabData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: 'Test Tab For Widget Updates',
pageLayoutId: layoutData.createPageLayout.id,
},
});
testSetup = {
pageLayoutId: layoutData.createPageLayout.id,
pageLayoutTabId: tabData.createPageLayoutTab.id,
...testFieldMetadataIds,
};
});
afterAll(async () => {
await destroyOnePageLayoutTab({
expectToFail: false,
input: { id: testSetup.pageLayoutTabId },
});
await destroyOnePageLayout({
expectToFail: false,
input: { id: testSetup.pageLayoutId },
});
});
describe('from an IFRAME widget', () => {
let testPageLayoutWidgetId: string;
beforeEach(async () => {
const { data } = await createOnePageLayoutWidget({
expectToFail: false,
input: {
title: 'Original Widget Title',
pageLayoutTabId: testSetup.pageLayoutTabId,
type: WidgetType.IFRAME,
configuration: TEST_IFRAME_CONFIG,
position: DEFAULT_GRID_POSITION,
},
});
testPageLayoutWidgetId = data.createPageLayoutWidget.id;
});
afterEach(async () => {
await destroyOnePageLayoutWidget({
expectToFail: false,
input: { id: testPageLayoutWidgetId },
});
});
it.each(eachTestingContextFilter(IFRAME_UPDATE_TEST_CASES))(
'should $title',
async ({ context: { input } }) => {
const { data } = await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: testPageLayoutWidgetId,
...input,
},
});
expect(data.updatePageLayoutWidget).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny({
...data.updatePageLayoutWidget,
}),
);
},
);
});
describe('from a STANDALONE_RICH_TEXT widget', () => {
let testPageLayoutWidgetId: string;
beforeEach(async () => {
const { data } = await createOnePageLayoutWidget({
expectToFail: false,
input: {
title: 'Original Rich Text Widget',
pageLayoutTabId: testSetup.pageLayoutTabId,
type: WidgetType.STANDALONE_RICH_TEXT,
configuration: {
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
body: { markdown: 'Initial content' },
},
position: DEFAULT_GRID_POSITION,
},
});
testPageLayoutWidgetId = data.createPageLayoutWidget.id;
});
afterEach(async () => {
await destroyOnePageLayoutWidget({
expectToFail: false,
input: { id: testPageLayoutWidgetId },
});
});
it('should update page layout widget to STANDALONE_RICH_TEXT configuration', async () => {
const { data } = await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: testPageLayoutWidgetId,
configuration: TEST_STANDALONE_RICH_TEXT_CONFIG,
},
});
expect(data.updatePageLayoutWidget).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny({
...data.updatePageLayoutWidget,
}),
);
});
});
describe('from a GRAPH widget', () => {
let testPageLayoutWidgetId: string;
const graphTestCases: EachTestingContext<GraphUpdateTestContext>[] = [
{
title: 'update page layout widget to AGGREGATE_CHART configuration',
context: {
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.AGGREGATE_CHART,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.COUNT,
label: 'Total Records',
description: 'Count of all records',
numberFormat: ChartNumberFormat.SHORT,
displayDataLabel: true,
}),
},
},
{
title: 'update page layout widget to VERTICAL BAR_CHART configuration',
context: {
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.BAR_CHART,
layout: BarChartLayout.VERTICAL,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.SUM,
primaryAxisGroupByFieldMetadataId: testSetup.fieldMetadataId2,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
displayDataLabel: true,
axisNameDisplay: AxisNameDisplay.NONE,
color: 'red',
description: 'Monthly revenue breakdown',
omitNullValues: true,
rangeMin: 0,
rangeMax: 100000,
}),
},
},
{
title:
'update page layout widget to HORIZONTAL BAR_CHART configuration',
context: {
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.BAR_CHART,
layout: BarChartLayout.HORIZONTAL,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.SUM,
primaryAxisGroupByFieldMetadataId: testSetup.fieldMetadataId2,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
displayDataLabel: true,
axisNameDisplay: AxisNameDisplay.NONE,
color: 'blue',
description: 'Horizontal revenue breakdown',
omitNullValues: true,
rangeMin: 0,
rangeMax: 100000,
}),
},
},
{
title: 'update page layout widget to PIE_CHART configuration',
context: {
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.PIE_CHART,
groupByFieldMetadataId: testSetup.fieldMetadataId1,
aggregateFieldMetadataId: testSetup.fieldMetadataId2,
aggregateOperation: AggregateOperations.SUM,
orderBy: GraphOrderBy.VALUE_DESC,
displayDataLabel: true,
displayLegend: true,
showCenterMetric: true,
color: 'yellow',
description: 'Distribution by category',
}),
},
},
{
title: 'update page layout widget to LINE_CHART configuration',
context: {
buildConfiguration: () => ({
configurationType: WidgetConfigurationType.LINE_CHART,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.AVG,
primaryAxisGroupByFieldMetadataId: testSetup.fieldMetadataId2,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
secondaryAxisGroupByFieldMetadataId: testSetup.fieldMetadataId3,
secondaryAxisGroupBySubFieldName:
testSetup.fieldMetadataId3SubFieldName,
secondaryAxisOrderBy: GraphOrderBy.FIELD_DESC,
displayDataLabel: true,
axisNameDisplay: AxisNameDisplay.NONE,
color: 'cyan',
description: 'Trend over time',
omitNullValues: false,
rangeMin: -100,
rangeMax: 100,
}),
},
},
];
beforeEach(async () => {
const { data } = await createOnePageLayoutWidget({
expectToFail: false,
input: {
title: 'Original Graph Widget',
pageLayoutTabId: testSetup.pageLayoutTabId,
type: WidgetType.GRAPH,
objectMetadataId: testSetup.objectMetadataId,
configuration: {
configurationType: WidgetConfigurationType.AGGREGATE_CHART,
aggregateFieldMetadataId: testSetup.fieldMetadataId1,
aggregateOperation: AggregateOperations.COUNT,
displayDataLabel: false,
},
position: DEFAULT_GRID_POSITION,
},
});
testPageLayoutWidgetId = data.createPageLayoutWidget.id;
});
afterEach(async () => {
await destroyOnePageLayoutWidget({
expectToFail: false,
input: { id: testPageLayoutWidgetId },
});
});
it.each(eachTestingContextFilter(graphTestCases))(
'should $title',
async ({ context: { buildConfiguration } }) => {
const { data } = await updateOnePageLayoutWidget({
expectToFail: false,
input: {
id: testPageLayoutWidgetId,
configuration: buildConfiguration(),
},
});
expect(data.updatePageLayoutWidget).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny({
...data.updatePageLayoutWidget,
}),
);
},
);
});
});

View File

@@ -0,0 +1,246 @@
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 { updateOnePageLayoutWidget } from 'test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget.util';
import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util';
import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util';
import { updateOnePageLayoutWithTabsAndWidgets } from 'test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets.util';
import {
PageLayoutTabLayoutMode,
PageLayoutType,
PageLayoutWidgetVerticalListHeightBehavior,
WidgetType,
} from 'twenty-shared/types';
import { v4 } from 'uuid';
import { type UpdatePageLayoutWidgetWithIdInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget-with-id.input';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
const LAYOUT_NAME = 'Viewport batch validation';
describe('Tab viewport widgets in metadata mutation batches', () => {
let pageLayoutId: string;
let pageLayoutTabId: string;
let viewportWidgetId: string;
const buildWidget = ({
id = v4(),
index,
heightBehavior = PageLayoutWidgetVerticalListHeightBehavior.FIT_CONTENT,
}: {
id?: string;
index: number;
heightBehavior?: PageLayoutWidgetVerticalListHeightBehavior;
}): UpdatePageLayoutWidgetWithIdInput => ({
id,
pageLayoutTabId,
title: 'Iframe widget',
type: WidgetType.IFRAME,
objectMetadataId: null,
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index,
heightBehavior,
},
configuration: { configurationType: WidgetConfigurationType.IFRAME },
});
const saveWidgets = async (widgets: UpdatePageLayoutWidgetWithIdInput[]) => {
const { data, errors } = await updateOnePageLayoutWithTabsAndWidgets({
expectToFail: false,
input: {
id: pageLayoutId,
name: LAYOUT_NAME,
type: PageLayoutType.RECORD_PAGE,
objectMetadataId: null,
tabs: [
{
id: pageLayoutTabId,
title: 'Overview',
position: 0,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets,
},
],
},
});
expect(errors).toBeUndefined();
return data.updatePageLayoutWithTabsAndWidgets.tabs?.[0].widgets;
};
beforeEach(async () => {
const { data: layoutData } = await createOnePageLayout({
expectToFail: false,
input: { name: LAYOUT_NAME, type: PageLayoutType.RECORD_PAGE },
});
pageLayoutId = layoutData.createPageLayout.id;
const { data: tabData } = await createOnePageLayoutTab({
expectToFail: false,
input: {
title: 'Overview',
pageLayoutId,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
},
});
pageLayoutTabId = tabData.createPageLayoutTab.id;
viewportWidgetId = v4();
await saveWidgets([
buildWidget({
id: viewportWidgetId,
index: 0,
heightBehavior: PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
}),
]);
});
afterEach(async () => {
await destroyOnePageLayoutTab({
expectToFail: false,
input: { id: pageLayoutTabId },
});
await destroyOnePageLayout({
expectToFail: false,
input: { id: pageLayoutId },
});
});
it('creates content above a viewport reindexed in the same save', async () => {
const content = buildWidget({ index: 0 });
const viewport = buildWidget({
id: viewportWidgetId,
index: 1,
heightBehavior: PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
});
const widgets = await saveWidgets([content, viewport]);
expect(widgets).toHaveLength(2);
expect(widgets).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: content.id, position: content.position }),
expect.objectContaining({
id: viewport.id,
position: viewport.position,
}),
]),
);
});
it('creates a viewport while the existing viewport becomes fit-content', async () => {
const content = buildWidget({ id: viewportWidgetId, index: 0 });
const viewport = buildWidget({
index: 1,
heightBehavior: PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
});
const widgets = await saveWidgets([content, viewport]);
expect(widgets).toHaveLength(2);
expect(widgets).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: content.id, position: content.position }),
expect.objectContaining({
id: viewport.id,
position: viewport.position,
}),
]),
);
});
it('transfers viewport height between existing widgets in one save', async () => {
const contentId = v4();
await saveWidgets([
buildWidget({ id: contentId, index: 0 }),
buildWidget({
id: viewportWidgetId,
index: 1,
heightBehavior: PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
}),
]);
const content = buildWidget({ id: viewportWidgetId, index: 0 });
const viewport = buildWidget({
id: contentId,
index: 1,
heightBehavior: PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
});
const widgets = await saveWidgets([content, viewport]);
expect(widgets).toHaveLength(2);
expect(widgets).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: content.id, position: content.position }),
expect.objectContaining({
id: viewport.id,
position: viewport.position,
}),
]),
);
});
it('replaces a viewport deleted in the same save', async () => {
const replacement = buildWidget({
index: 0,
heightBehavior: PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
});
const widgets = await saveWidgets([replacement]);
expect(widgets).toEqual([
expect.objectContaining({
id: replacement.id,
position: replacement.position,
}),
]);
});
it('keeps untouched viewport siblings when validating a single creation', async () => {
const { id: _id, ...input } = buildWidget({
index: 1,
heightBehavior: PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
});
const { errors } = await createOnePageLayoutWidget({
expectToFail: true,
input,
});
expect(errors).toEqual(
expect.arrayContaining([
expect.objectContaining({
extensions: expect.objectContaining({
code: 'METADATA_VALIDATION_FAILED',
userFriendlyMessage:
'Only one full-height widget is allowed per tab',
}),
}),
]),
);
});
it('keeps untouched viewport siblings when validating a single update', async () => {
const content = buildWidget({ index: 0 });
await saveWidgets([
content,
buildWidget({
id: viewportWidgetId,
index: 1,
heightBehavior: PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
}),
]);
const { errors } = await updateOnePageLayoutWidget({
expectToFail: true,
input: {
id: content.id,
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 0,
heightBehavior:
PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
},
},
});
expect(errors).toEqual(
expect.arrayContaining([
expect.objectContaining({
extensions: expect.objectContaining({
code: 'METADATA_VALIDATION_FAILED',
userFriendlyMessage:
'Only one full-height widget is allowed per tab',
}),
}),
]),
);
});
});

View File

@@ -0,0 +1,40 @@
import { WIDGET_POSITION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-position-gql-fields.constant';
import gql from 'graphql-tag';
import { WIDGET_CONFIGURATION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { type CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
export type CreateOnePageLayoutWidgetFactoryInput = CreatePageLayoutWidgetInput;
const DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS = `
id
title
type
pageLayoutTabId
objectMetadataId
position {
${WIDGET_POSITION_GQL_FIELDS}
}
configuration {
${WIDGET_CONFIGURATION_GQL_FIELDS}
}
createdAt
updatedAt
deletedAt
`;
export const createOnePageLayoutWidgetQueryFactory = ({
input,
gqlFields = DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS,
}: PerformMetadataQueryParams<CreateOnePageLayoutWidgetFactoryInput>) => ({
query: gql`
mutation CreatePageLayoutWidget($input: CreatePageLayoutWidgetInput!) {
createPageLayoutWidget(input: $input) {
${gqlFields}
}
}
`,
variables: {
input,
},
});

View File

@@ -0,0 +1,44 @@
import {
type CreateOnePageLayoutWidgetFactoryInput,
createOnePageLayoutWidgetQueryFactory,
} from 'test/integration/metadata/suites/page-layout-widget/utils/create-one-page-layout-widget-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
export const createOnePageLayoutWidget = async ({
input,
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<CreateOnePageLayoutWidgetFactoryInput>): CommonResponseBody<{
createPageLayoutWidget: PageLayoutWidgetDTO;
}> => {
const graphqlOperation = createOnePageLayoutWidgetQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage:
'Page layout widget creation should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Page layout widget creation has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,19 @@
import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
export type DestroyOnePageLayoutWidgetFactoryInput = {
id: string;
};
export const destroyOnePageLayoutWidgetQueryFactory = ({
input,
}: PerformMetadataQueryParams<DestroyOnePageLayoutWidgetFactoryInput>) => ({
query: gql`
mutation DestroyPageLayoutWidget($id: String!) {
destroyPageLayoutWidget(id: $id)
}
`,
variables: {
id: input.id,
},
});

View File

@@ -0,0 +1,39 @@
import {
type DestroyOnePageLayoutWidgetFactoryInput,
destroyOnePageLayoutWidgetQueryFactory,
} from 'test/integration/metadata/suites/page-layout-widget/utils/destroy-one-page-layout-widget-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
export const destroyOnePageLayoutWidget = async ({
input,
expectToFail = false,
token,
}: PerformMetadataQueryParams<DestroyOnePageLayoutWidgetFactoryInput>): CommonResponseBody<{
destroyPageLayoutWidget: boolean;
}> => {
const graphqlOperation = destroyOnePageLayoutWidgetQueryFactory({
input,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Page layout widget destroy should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Page layout widget destroy has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,61 @@
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
export type TestFieldMetadataIds = {
objectMetadataId: string;
fieldMetadataId1: string;
fieldMetadataId2: string;
fieldMetadataId3: string;
fieldMetadataId3SubFieldName: string;
};
// Uses well-known company fields that make semantic sense for chart configs:
// - position (number field, suitable for aggregation)
// - name (text field, suitable for grouping)
// - domainName (link field, suitable for secondary grouping)
export const fetchTestFieldMetadataIds =
async (): Promise<TestFieldMetadataIds> => {
const { objects } = await findManyObjectMetadata({
expectToFail: false,
input: {
filter: {},
paging: { first: 100 },
},
gqlFields: `
id
nameSingular
fieldsList {
id
name
}
`,
});
const companyObject = objects.find(
(object) => object.nameSingular === 'company',
);
jestExpectToBeDefined(companyObject);
const fieldsList = companyObject.fieldsList;
jestExpectToBeDefined(fieldsList);
const findFieldByName = (fieldName: string) => {
const field = fieldsList!.find(
(fieldMetadata) => fieldMetadata.name === fieldName,
);
jestExpectToBeDefined(field);
return field!;
};
return {
objectMetadataId: companyObject.id,
fieldMetadataId1: findFieldByName('position').id,
fieldMetadataId2: findFieldByName('name').id,
fieldMetadataId3: findFieldByName('domainName').id,
fieldMetadataId3SubFieldName: 'primaryLinkUrl',
};
};

View File

@@ -0,0 +1,41 @@
import { WIDGET_POSITION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-position-gql-fields.constant';
import gql from 'graphql-tag';
import { WIDGET_CONFIGURATION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
export type FindPageLayoutWidgetsFactoryInput = {
pageLayoutTabId: string;
};
const DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS = `
id
title
type
pageLayoutTabId
objectMetadataId
position {
${WIDGET_POSITION_GQL_FIELDS}
}
configuration {
${WIDGET_CONFIGURATION_GQL_FIELDS}
}
createdAt
updatedAt
deletedAt
`;
export const findPageLayoutWidgetsQueryFactory = ({
input,
gqlFields = DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS,
}: PerformMetadataQueryParams<FindPageLayoutWidgetsFactoryInput>) => ({
query: gql`
query GetPageLayoutWidgets($pageLayoutTabId: String!) {
getPageLayoutWidgets(pageLayoutTabId: $pageLayoutTabId) {
${gqlFields}
}
}
`,
variables: {
pageLayoutTabId: input.pageLayoutTabId,
},
});

View File

@@ -0,0 +1,43 @@
import {
type FindPageLayoutWidgetsFactoryInput,
findPageLayoutWidgetsQueryFactory,
} from 'test/integration/metadata/suites/page-layout-widget/utils/find-page-layout-widgets-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
export const findPageLayoutWidgets = async ({
input,
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<FindPageLayoutWidgetsFactoryInput>): CommonResponseBody<{
getPageLayoutWidgets: PageLayoutWidgetDTO[];
}> => {
const graphqlOperation = findPageLayoutWidgetsQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Find page layout widgets should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Find page layout widgets has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

View File

@@ -0,0 +1,54 @@
import { WIDGET_POSITION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-position-gql-fields.constant';
import gql from 'graphql-tag';
import { WIDGET_CONFIGURATION_GQL_FIELDS } from 'test/integration/metadata/suites/page-layout-widget/constants/widget-configuration-gql-fields.constant';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { type UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input';
export type UpdateOnePageLayoutWidgetFactoryInput = {
id: string;
} & UpdatePageLayoutWidgetInput;
const DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS = `
id
title
type
pageLayoutTabId
objectMetadataId
position {
${WIDGET_POSITION_GQL_FIELDS}
}
configuration {
${WIDGET_CONFIGURATION_GQL_FIELDS}
}
createdAt
updatedAt
deletedAt
`;
export const updateOnePageLayoutWidgetQueryFactory = ({
input,
gqlFields = DEFAULT_PAGE_LAYOUT_WIDGET_GQL_FIELDS,
}: PerformMetadataQueryParams<UpdateOnePageLayoutWidgetFactoryInput>) => ({
query: gql`
mutation UpdatePageLayoutWidget($id: String!, $input: UpdatePageLayoutWidgetInput!) {
updatePageLayoutWidget(id: $id, input: $input) {
${gqlFields}
}
}
`,
variables: {
id: input.id,
input: {
pageLayoutTabId: input.pageLayoutTabId,
title: input.title,
type: input.type,
objectMetadataId: input.objectMetadataId,
position: input.position,
configuration: input.configuration,
conditionalDisplay: input.conditionalDisplay,
conditionalAvailabilityExpression:
input.conditionalAvailabilityExpression,
},
},
});

View File

@@ -0,0 +1,43 @@
import {
type UpdateOnePageLayoutWidgetFactoryInput,
updateOnePageLayoutWidgetQueryFactory,
} from 'test/integration/metadata/suites/page-layout-widget/utils/update-one-page-layout-widget-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
export const updateOnePageLayoutWidget = async ({
input,
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<UpdateOnePageLayoutWidgetFactoryInput>): CommonResponseBody<{
updatePageLayoutWidget: PageLayoutWidgetDTO;
}> => {
const graphqlOperation = updateOnePageLayoutWidgetQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Page layout widget update should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Page layout widget update has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};