diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts index 344933b0e0..e23a20634c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts @@ -7,6 +7,7 @@ import { v4 } from 'uuid'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util'; @@ -123,11 +124,20 @@ export class PageLayoutUpdateService { }, ); + const optimisticFlatPageLayoutTabMaps = tabsToCreate.reduce( + (maps, tab) => + addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity: tab, + flatEntityMaps: maps, + }), + flatPageLayoutTabMaps, + ); + const { widgetsToCreate, widgetsToUpdate, widgetsToDelete } = this.computeWidgetOperationsForAllTabs({ tabs, flatPageLayoutWidgetMaps, - flatPageLayoutTabMaps, + flatPageLayoutTabMaps: optimisticFlatPageLayoutTabMaps, flatObjectMetadataMaps, workspaceId, workspaceCustomApplicationId: workspaceCustomFlatApplication.id, diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-creation-via-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-creation-via-update.integration-spec.ts.snap new file mode 100644 index 0000000000..7cbaff1e69 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-with-tabs-creation-via-update.integration-spec.ts.snap @@ -0,0 +1,91 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Page layout with tabs creation via update should succeed should create a new tab with a widget via page layout update 1`] = ` +{ + "createdAt": Any, + "deletedAt": null, + "id": Any, + "name": "Layout With New Tab", + "objectMetadataId": null, + "tabs": [ + { + "createdAt": Any, + "deletedAt": null, + "id": Any, + "pageLayoutId": Any, + "position": 0, + "title": "Existing Tab", + "updatedAt": Any, + "widgets": [ + { + "configuration": { + "aggregateFieldMetadataId": Any, + "aggregateOperation": "COUNT", + "color": null, + "configurationType": "PIE_CHART", + "dateGranularity": "DAY", + "description": null, + "displayDataLabel": false, + "displayLegend": true, + "filter": null, + "firstDayOfTheWeek": 1, + "groupByFieldMetadataId": Any, + "groupBySubFieldName": null, + "manualSortOrder": null, + "orderBy": "VALUE_DESC", + "showCenterMetric": true, + "timezone": "UTC", + }, + "createdAt": Any, + "deletedAt": null, + "gridPosition": { + "column": 0, + "columnSpan": 1, + "row": 0, + "rowSpan": 1, + }, + "id": Any, + "objectMetadataId": Any, + "pageLayoutTabId": Any, + "title": "Existing Tab Widget", + "type": "GRAPH", + "updatedAt": Any, + }, + ], + }, + { + "createdAt": Any, + "deletedAt": null, + "id": Any, + "pageLayoutId": Any, + "position": 1, + "title": "New Tab Created Via Update", + "updatedAt": Any, + "widgets": [ + { + "configuration": { + "configurationType": "IFRAME", + "url": "https://example.com", + }, + "createdAt": Any, + "deletedAt": null, + "gridPosition": { + "column": 0, + "columnSpan": 1, + "row": 0, + "rowSpan": 1, + }, + "id": Any, + "objectMetadataId": null, + "pageLayoutTabId": Any, + "title": "New Tab Iframe Widget", + "type": "IFRAME", + "updatedAt": Any, + }, + ], + }, + ], + "type": "RECORD_PAGE", + "updatedAt": Any, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-with-tabs-creation-via-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-with-tabs-creation-via-update.integration-spec.ts new file mode 100644 index 0000000000..7c3c4166b7 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-with-tabs-creation-via-update.integration-spec.ts @@ -0,0 +1,157 @@ +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 { 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 { updateOnePageLayoutWithTabsAndWidgets } from 'test/integration/metadata/suites/page-layout/utils/update-one-page-layout-with-tabs-and-widgets.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { v4 } from 'uuid'; + +import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type'; +import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum'; +import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; + +describe('Page layout with tabs creation via update should succeed', () => { + let testFieldMetadataIds: { + objectMetadataId: string; + fieldMetadataId1: string; + fieldMetadataId2: string; + }; + let testPageLayoutId: string; + let existingTabId: string; + let newTabId: string; + + beforeAll(async () => { + const allFieldMetadataIds = await fetchTestFieldMetadataIds(); + + testFieldMetadataIds = { + objectMetadataId: allFieldMetadataIds.objectMetadataId, + fieldMetadataId1: allFieldMetadataIds.fieldMetadataId1, + fieldMetadataId2: allFieldMetadataIds.fieldMetadataId2, + }; + + const { data: layoutData } = await createOnePageLayout({ + expectToFail: false, + input: { + name: 'Test Page Layout', + type: PageLayoutType.RECORD_PAGE, + }, + }); + + testPageLayoutId = layoutData.createPageLayout.id; + + const { data: tabData } = await createOnePageLayoutTab({ + expectToFail: false, + input: { + title: 'Initial Tab', + pageLayoutId: testPageLayoutId, + }, + }); + + existingTabId = tabData.createPageLayoutTab.id; + }); + + afterAll(async () => { + if (newTabId) { + await destroyOnePageLayoutTab({ + expectToFail: null, + input: { id: newTabId }, + }); + } + + await destroyOnePageLayoutTab({ + expectToFail: false, + input: { id: existingTabId }, + }); + + await destroyOnePageLayout({ + expectToFail: false, + input: { id: testPageLayoutId }, + }); + }); + + it('should create a new tab with a widget via page layout update', async () => { + const existingTabWidgetId = v4(); + + newTabId = v4(); + const newTabWidgetId = v4(); + + const { data } = await updateOnePageLayoutWithTabsAndWidgets({ + expectToFail: false, + input: { + id: testPageLayoutId, + name: 'Layout With New Tab', + type: PageLayoutType.RECORD_PAGE, + objectMetadataId: null, + tabs: [ + { + id: existingTabId, + title: 'Existing Tab', + position: 0, + widgets: [ + { + id: existingTabWidgetId, + pageLayoutTabId: existingTabId, + title: 'Existing Tab Widget', + type: WidgetType.GRAPH, + objectMetadataId: testFieldMetadataIds.objectMetadataId, + gridPosition: { + row: 0, + column: 0, + rowSpan: 1, + columnSpan: 1, + }, + configuration: { + configurationType: WidgetConfigurationType.PIE_CHART, + aggregateFieldMetadataId: + testFieldMetadataIds.fieldMetadataId1, + aggregateOperation: AggregateOperations.COUNT, + groupByFieldMetadataId: testFieldMetadataIds.fieldMetadataId2, + }, + }, + ], + }, + { + id: newTabId, + title: 'New Tab Created Via Update', + position: 1, + widgets: [ + { + id: newTabWidgetId, + pageLayoutTabId: newTabId, + title: 'New Tab Iframe Widget', + type: WidgetType.IFRAME, + objectMetadataId: null, + gridPosition: { + row: 0, + column: 0, + rowSpan: 1, + columnSpan: 1, + }, + configuration: { + configurationType: WidgetConfigurationType.IFRAME, + url: 'https://example.com', + }, + }, + ], + }, + ], + }, + }); + + const result = data.updatePageLayoutWithTabsAndWidgets; + + expect(result.tabs).toHaveLength(2); + + for (const tab of result.tabs!) { + expect(tab.widgets!.length).toBeGreaterThanOrEqual(1); + } + + expect(result).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ + ...result, + }), + ); + }); +});