diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command.ts index b57def03e9..1c8eebe961 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command.ts @@ -1,20 +1,30 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Command } from 'nest-commander'; -import { ViewType, FeatureFlagKey } from 'twenty-shared/types'; +import { FeatureFlagKey, ViewType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; +import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; +import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type'; +import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util'; +import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util'; +import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util'; import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; +import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type'; +import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type'; @Command({ name: 'upgrade:1-19:backfill-page-layouts', @@ -30,6 +40,7 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra private readonly applicationService: ApplicationService, private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly featureFlagService: FeatureFlagService, + private readonly workspaceCacheService: WorkspaceCacheService, ) { super(workspaceRepository, twentyORMGlobalManager, dataSourceService); } @@ -65,7 +76,7 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra return; } - const { twentyStandardFlatApplication } = + const { twentyStandardFlatApplication, workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, ); @@ -199,13 +210,18 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra if (validateAndBuildResult.status === 'fail') { this.logger.error( - `Failed to create page layouts:\n${JSON.stringify(validateAndBuildResult, null, 2)}`, + `Failed to create standard page layouts:\n${JSON.stringify(validateAndBuildResult, null, 2)}`, ); throw new Error( - `Failed to create page layouts for workspace ${workspaceId}`, + `Failed to create standard page layouts for workspace ${workspaceId}`, ); } + await this.backfillCustomObjectPageLayouts({ + workspaceId, + workspaceCustomFlatApplication, + }); + await this.featureFlagService.enableFeatureFlags( [FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED], workspaceId, @@ -215,4 +231,149 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra `Successfully created page layouts and enabled IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`, ); } + + private async backfillCustomObjectPageLayouts({ + workspaceId, + workspaceCustomFlatApplication, + }: { + workspaceId: string; + workspaceCustomFlatApplication: FlatApplication; + }): Promise { + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatPageLayoutMaps, + } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatObjectMetadataMaps', + 'flatFieldMetadataMaps', + 'flatPageLayoutMaps', + ]); + + const existingPageLayouts = Object.values( + flatPageLayoutMaps.byUniversalIdentifier, + ).filter(isDefined); + + const objectIdsWithRecordPageLayout = new Set( + existingPageLayouts + .filter( + (layout: FlatPageLayout) => + layout.type === PageLayoutType.RECORD_PAGE && + isDefined(layout.objectMetadataId), + ) + .map((layout: FlatPageLayout) => layout.objectMetadataId), + ); + + const customObjectsWithoutPageLayout = Object.values( + flatObjectMetadataMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter( + (objectMetadata) => + objectMetadata.isCustom && + !objectMetadata.isRemote && + !objectIdsWithRecordPageLayout.has(objectMetadata.id), + ); + + if (customObjectsWithoutPageLayout.length === 0) { + this.logger.log( + `No custom objects without page layouts found for workspace ${workspaceId}`, + ); + + return; + } + + this.logger.log( + `Creating page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`, + ); + + const allCustomPageLayoutsToCreate: FlatPageLayout[] = []; + const allCustomPageLayoutTabsToCreate: FlatPageLayoutTab[] = []; + const allCustomPageLayoutWidgetsToCreate: FlatPageLayoutWidget[] = []; + const allCustomViewsToCreate: (UniversalFlatView & { id: string })[] = []; + const allCustomViewFieldsToCreate: UniversalFlatViewField[] = []; + + for (const customObject of customObjectsWithoutPageLayout) { + const flatRecordPageFieldsView = computeFlatRecordPageFieldsViewToCreate({ + objectMetadata: customObject, + flatApplication: workspaceCustomFlatApplication, + }); + + const objectFieldMetadatas = Object.values( + flatFieldMetadataMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter((field) => field.objectMetadataId === customObject.id); + + const viewFields = computeFlatViewFieldsToCreate({ + objectFlatFieldMetadatas: objectFieldMetadatas, + viewUniversalIdentifier: flatRecordPageFieldsView.universalIdentifier, + flatApplication: workspaceCustomFlatApplication, + labelIdentifierFieldMetadataUniversalIdentifier: + customObject.labelIdentifierFieldMetadataUniversalIdentifier, + }); + + const { pageLayouts, pageLayoutTabs, pageLayoutWidgets } = + computeFlatDefaultRecordPageLayoutToCreate({ + objectMetadata: customObject, + flatApplication: workspaceCustomFlatApplication, + recordPageFieldsView: flatRecordPageFieldsView, + workspaceId, + }); + + allCustomPageLayoutsToCreate.push(...pageLayouts); + allCustomPageLayoutTabsToCreate.push(...pageLayoutTabs); + allCustomPageLayoutWidgetsToCreate.push(...pageLayoutWidgets); + allCustomViewsToCreate.push(flatRecordPageFieldsView); + allCustomViewFieldsToCreate.push(...viewFields); + } + + const customValidateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + pageLayout: { + flatEntityToCreate: allCustomPageLayoutsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + pageLayoutTab: { + flatEntityToCreate: allCustomPageLayoutTabsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + pageLayoutWidget: { + flatEntityToCreate: allCustomPageLayoutWidgetsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + view: { + flatEntityToCreate: allCustomViewsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + viewField: { + flatEntityToCreate: allCustomViewFieldsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, + workspaceId, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }, + ); + + if (customValidateAndBuildResult.status === 'fail') { + this.logger.error( + `Failed to create custom object page layouts:\n${JSON.stringify(customValidateAndBuildResult, null, 2)}`, + ); + throw new Error( + `Failed to create custom object page layouts for workspace ${workspaceId}`, + ); + } + + this.logger.log( + `Successfully created page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`, + ); + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index c9702d8a25..3dcce1b040 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -3,10 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { + FeatureFlagKey, ViewOpenRecordIn, ViewType, ViewVisibility, - FeatureFlagKey, } from 'twenty-shared/types'; import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils'; import { FindManyOptions, FindOneOptions, Repository } from 'typeorm'; @@ -30,7 +30,6 @@ import { fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities } from import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type'; -import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant'; import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; @@ -39,16 +38,13 @@ import { ObjectMetadataException, ObjectMetadataExceptionCode, } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type'; -import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; +import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util'; +import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util'; +import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util'; import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; -import { - TAB_PROPS, - WIDGET_PROPS, -} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; @@ -645,40 +641,10 @@ export class ObjectMetadataService extends TypeOrmQueryService tab.id), - tabUniversalIdentifiers: pageLayoutTabs.map( - (tab) => tab.universalIdentifier, - ), - createdAt: now, - updatedAt: now, - deletedAt: null, - defaultTabToFocusOnMobileAndSidePanelId: null, - defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null, - }; - - return { pageLayouts: [pageLayout], pageLayoutTabs, pageLayoutWidgets }; + }); } private async computeFlatViewFieldsToCreate({ @@ -826,32 +681,12 @@ export class ObjectMetadataService extends TypeOrmQueryService - field.name !== 'deletedAt' && - // Include 'id' only if it's the label identifier (e.g., for junction tables) - (field.name !== 'id' || - field.universalIdentifier === - labelIdentifierFieldMetadataUniversalIdentifier), - ) - .map((field, index) => ({ - fieldMetadataUniversalIdentifier: field.universalIdentifier, - viewUniversalIdentifier, - viewFieldGroupUniversalIdentifier: null, - createdAt, - updatedAt: createdAt, - deletedAt: null, - universalIdentifier: v4(), - isVisible: true, - size: DEFAULT_VIEW_FIELD_SIZE, - position: index, - aggregateOperation: null, - applicationUniversalIdentifier: flatApplication.universalIdentifier, - })); - - return defaultViewFields; + return computeFlatViewFieldsToCreate({ + objectFlatFieldMetadatas, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier, + }); } private async computeFlatNavigationMenuItemToCreate({ diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util.ts new file mode 100644 index 0000000000..6ba2ec9b72 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util.ts @@ -0,0 +1,148 @@ +import { v4 } from 'uuid'; + +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; +import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; +import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type'; +import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type'; +import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; +import { + TAB_PROPS, + WIDGET_PROPS, +} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template'; +import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; +import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type'; + +export const computeFlatDefaultRecordPageLayoutToCreate = ({ + objectMetadata, + flatApplication, + recordPageFieldsView, + workspaceId, +}: { + flatApplication: FlatApplication; + objectMetadata: UniversalFlatObjectMetadata & { id: string }; + recordPageFieldsView: UniversalFlatView & { id: string }; + workspaceId: string; +}): { + pageLayouts: FlatPageLayout[]; + pageLayoutTabs: FlatPageLayoutTab[]; + pageLayoutWidgets: FlatPageLayoutWidget[]; +} => { + const now = new Date().toISOString(); + const pageLayoutId = v4(); + const pageLayoutUniversalIdentifier = v4(); + + const tabDefinitions = [ + { key: 'home' as const, widgetKey: 'fields' as const }, + { key: 'timeline' as const, widgetKey: 'timeline' as const }, + { key: 'tasks' as const, widgetKey: 'tasks' as const }, + { key: 'notes' as const, widgetKey: 'notes' as const }, + { key: 'files' as const, widgetKey: 'files' as const }, + { key: 'emails' as const, widgetKey: 'emails' as const }, + { key: 'calendar' as const, widgetKey: 'calendar' as const }, + ]; + + const pageLayoutTabs: FlatPageLayoutTab[] = []; + const pageLayoutWidgets: FlatPageLayoutWidget[] = []; + + for (const { key, widgetKey } of tabDefinitions) { + const tabProps = TAB_PROPS[key]; + const widgetProps = WIDGET_PROPS[widgetKey]; + const tabId = v4(); + const tabUniversalIdentifier = v4(); + const widgetId = v4(); + const widgetUniversalIdentifier = v4(); + + pageLayoutTabs.push({ + id: tabId, + universalIdentifier: tabUniversalIdentifier, + applicationId: flatApplication.id, + applicationUniversalIdentifier: flatApplication.universalIdentifier, + workspaceId, + title: tabProps.title, + position: tabProps.position, + pageLayoutId, + pageLayoutUniversalIdentifier, + widgetIds: [widgetId], + widgetUniversalIdentifiers: [widgetUniversalIdentifier], + createdAt: now, + updatedAt: now, + deletedAt: null, + icon: tabProps.icon, + layoutMode: tabProps.layoutMode, + }); + + const isFieldsWidget = widgetKey === 'fields'; + + const configuration = isFieldsWidget + ? { + configurationType: WidgetConfigurationType.FIELDS, + viewId: recordPageFieldsView.id, + } + : { + configurationType: + WidgetConfigurationType[ + widgetKey.toUpperCase() as keyof typeof WidgetConfigurationType + ], + }; + + const universalConfiguration = isFieldsWidget + ? { + configurationType: WidgetConfigurationType.FIELDS, + viewId: recordPageFieldsView.universalIdentifier, + } + : { + configurationType: + WidgetConfigurationType[ + widgetKey.toUpperCase() as keyof typeof WidgetConfigurationType + ], + }; + + pageLayoutWidgets.push({ + id: widgetId, + universalIdentifier: widgetUniversalIdentifier, + applicationId: flatApplication.id, + applicationUniversalIdentifier: flatApplication.universalIdentifier, + workspaceId, + pageLayoutTabId: tabId, + pageLayoutTabUniversalIdentifier: tabUniversalIdentifier, + title: widgetProps.title, + type: widgetProps.type, + gridPosition: widgetProps.gridPosition, + position: widgetProps.position, + // @ts-expect-error - configurationType is validated but TS can't match to discriminated union + configuration, + // @ts-expect-error - configurationType is validated but TS can't match to discriminated union + universalConfiguration, + objectMetadataId: objectMetadata.id, + objectMetadataUniversalIdentifier: objectMetadata.universalIdentifier, + createdAt: now, + updatedAt: now, + deletedAt: null, + conditionalDisplay: null, + }); + } + + const pageLayout: FlatPageLayout = { + id: pageLayoutId, + universalIdentifier: pageLayoutUniversalIdentifier, + applicationId: flatApplication.id, + applicationUniversalIdentifier: flatApplication.universalIdentifier, + workspaceId, + name: `Default ${objectMetadata.labelSingular} Layout`, + type: PageLayoutType.RECORD_PAGE, + objectMetadataId: objectMetadata.id, + objectMetadataUniversalIdentifier: objectMetadata.universalIdentifier, + tabIds: pageLayoutTabs.map((tab) => tab.id), + tabUniversalIdentifiers: pageLayoutTabs.map( + (tab) => tab.universalIdentifier, + ), + createdAt: now, + updatedAt: now, + deletedAt: null, + defaultTabToFocusOnMobileAndSidePanelId: null, + defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null, + }; + + return { pageLayouts: [pageLayout], pageLayoutTabs, pageLayoutWidgets }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util.ts new file mode 100644 index 0000000000..25fa0cc677 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util.ts @@ -0,0 +1,53 @@ +import { + ViewOpenRecordIn, + ViewType, + ViewVisibility, +} from 'twenty-shared/types'; +import { v4 } from 'uuid'; + +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; +import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type'; + +export const computeFlatRecordPageFieldsViewToCreate = ({ + objectMetadata, + flatApplication, +}: { + flatApplication: FlatApplication; + objectMetadata: UniversalFlatObjectMetadata & { id: string }; +}): UniversalFlatView & { id: string } => { + const createdAt = new Date().toISOString(); + + return { + id: v4(), + objectMetadataUniversalIdentifier: objectMetadata.universalIdentifier, + name: `${objectMetadata.labelSingular} Record Page Fields`, + key: null, + icon: 'IconList', + type: ViewType.FIELDS_WIDGET, + createdAt, + updatedAt: createdAt, + deletedAt: null, + isCustom: true, + anyFieldFilterValue: null, + calendarFieldMetadataUniversalIdentifier: null, + calendarLayout: null, + isCompact: false, + shouldHideEmptyGroups: false, + kanbanAggregateOperation: null, + kanbanAggregateOperationFieldMetadataUniversalIdentifier: null, + mainGroupByFieldMetadataUniversalIdentifier: null, + openRecordIn: ViewOpenRecordIn.SIDE_PANEL, + position: 0, + universalIdentifier: v4(), + visibility: ViewVisibility.WORKSPACE, + createdByUserWorkspaceId: null, + viewFieldUniversalIdentifiers: [], + viewFieldGroupUniversalIdentifiers: [], + viewFilterUniversalIdentifiers: [], + viewGroupUniversalIdentifiers: [], + viewFilterGroupUniversalIdentifiers: [], + viewSortUniversalIdentifiers: [], + applicationUniversalIdentifier: flatApplication.universalIdentifier, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util.ts new file mode 100644 index 0000000000..81d7ec9e10 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util.ts @@ -0,0 +1,45 @@ +import { v4 } from 'uuid'; + +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant'; +import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; +import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type'; + +export const computeFlatViewFieldsToCreate = ({ + objectFlatFieldMetadatas, + viewUniversalIdentifier, + flatApplication, + labelIdentifierFieldMetadataUniversalIdentifier, +}: { + flatApplication: FlatApplication; + objectFlatFieldMetadatas: UniversalFlatFieldMetadata[]; + viewUniversalIdentifier: string; + labelIdentifierFieldMetadataUniversalIdentifier: string | null; +}): UniversalFlatViewField[] => { + const createdAt = new Date().toISOString(); + const defaultViewFields = objectFlatFieldMetadatas + .filter( + (field) => + field.name !== 'deletedAt' && + // Include 'id' only if it's the label identifier (e.g., for junction tables) + (field.name !== 'id' || + field.universalIdentifier === + labelIdentifierFieldMetadataUniversalIdentifier), + ) + .map((field, index) => ({ + fieldMetadataUniversalIdentifier: field.universalIdentifier, + viewUniversalIdentifier, + viewFieldGroupUniversalIdentifier: null, + createdAt, + updatedAt: createdAt, + deletedAt: null, + universalIdentifier: v4(), + isVisible: true, + size: DEFAULT_VIEW_FIELD_SIZE, + position: index, + aggregateOperation: null, + applicationUniversalIdentifier: flatApplication.universalIdentifier, + })); + + return defaultViewFields; +};