Allow users to create Fields and Field widget (#18801)
- Allow users to create a Fields widget or a Field widget; **this PR is focused on Fields widgets as Field widget can't be configured yet** - Automatically create a filled view when the user creates a draft Fields widget in edit mode https://github.com/user-attachments/assets/b2dbba52-c614-44cd-bf6c-095ce9d4ec26 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
committed by
GitHub
parent
dc00701448
commit
e9aa6f47e5
@@ -16,6 +16,7 @@ import { PageLayoutDuplicationService } from 'src/engine/metadata-modules/page-l
|
||||
import { PageLayoutUpdateService } from 'src/engine/metadata-modules/page-layout/services/page-layout-update.service';
|
||||
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
@@ -37,6 +38,7 @@ import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.m
|
||||
FlatPageLayoutWidgetModule,
|
||||
ApplicationModule,
|
||||
DashboardSyncModule,
|
||||
ViewModule,
|
||||
],
|
||||
controllers: [PageLayoutController],
|
||||
providers: [
|
||||
|
||||
+121
-1
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
import { computeDiffBetweenObjects, isDefined } from 'twenty-shared/utils';
|
||||
@@ -21,6 +21,7 @@ import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layou
|
||||
import { reconstructFlatPageLayoutWithTabsAndWidgets } from 'src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util';
|
||||
import { UpdatePageLayoutTabWithWidgetsInput } from 'src/engine/metadata-modules/page-layout-tab/dtos/inputs/update-page-layout-tab-with-widgets.input';
|
||||
import { 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';
|
||||
import { UpdatePageLayoutWithTabsInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input';
|
||||
import { PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto';
|
||||
import {
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
generatePageLayoutExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
|
||||
import { fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.util';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
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 { DashboardSyncService } from 'src/modules/dashboard-sync/services/dashboard-sync.service';
|
||||
@@ -42,11 +44,14 @@ type UpdatePageLayoutWithTabsParams = {
|
||||
|
||||
@Injectable()
|
||||
export class PageLayoutUpdateService {
|
||||
private readonly logger = new Logger(PageLayoutUpdateService.name);
|
||||
|
||||
constructor(
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly dashboardSyncService: DashboardSyncService,
|
||||
private readonly viewService: ViewService,
|
||||
) {}
|
||||
|
||||
async updatePageLayoutWithTabs({
|
||||
@@ -161,6 +166,12 @@ export class PageLayoutUpdateService {
|
||||
flatViewMaps,
|
||||
});
|
||||
|
||||
const orphanedViewIds = this.collectOrphanedViewIdsFromDeletedWidgets({
|
||||
widgetsToUpdate,
|
||||
tabsToUpdate,
|
||||
flatPageLayoutWidgetMaps,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
@@ -223,6 +234,11 @@ export class PageLayoutUpdateService {
|
||||
},
|
||||
);
|
||||
|
||||
await this.destroyOrphanedFieldsWidgetViews({
|
||||
viewIds: orphanedViewIds,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto(
|
||||
reconstructFlatPageLayoutWithTabsAndWidgets({
|
||||
layout: flatLayout,
|
||||
@@ -617,4 +633,108 @@ export class PageLayoutUpdateService {
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private collectOrphanedViewIdsFromDeletedWidgets({
|
||||
widgetsToUpdate,
|
||||
tabsToUpdate,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}: {
|
||||
widgetsToUpdate: FlatPageLayoutWidget[];
|
||||
tabsToUpdate: FlatPageLayoutTab[];
|
||||
flatPageLayoutWidgetMaps: Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatPageLayoutWidgetMaps'
|
||||
>['flatPageLayoutWidgetMaps'];
|
||||
}): string[] {
|
||||
const viewIdsToDelete = new Set<string>();
|
||||
const directlyDeletedWidgetIds = new Set<string>();
|
||||
|
||||
// Collect viewIds from directly deleted FIELDS widgets
|
||||
for (const widget of widgetsToUpdate) {
|
||||
if (isDefined(widget.deletedAt)) {
|
||||
directlyDeletedWidgetIds.add(widget.id);
|
||||
const viewId = this.getViewIdFromFieldsWidget(widget);
|
||||
|
||||
if (isDefined(viewId)) {
|
||||
viewIdsToDelete.add(viewId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect viewIds from FIELDS widgets in deleted tabs
|
||||
const deletedTabIds = new Set(
|
||||
tabsToUpdate
|
||||
.filter((tab) => isDefined(tab.deletedAt))
|
||||
.map((tab) => tab.id),
|
||||
);
|
||||
|
||||
const allExistingWidgets = Object.values(
|
||||
flatPageLayoutWidgetMaps.byUniversalIdentifier,
|
||||
).filter(isDefined);
|
||||
|
||||
for (const widget of allExistingWidgets) {
|
||||
if (
|
||||
!isDefined(widget.deletedAt) &&
|
||||
deletedTabIds.has(widget.pageLayoutTabId)
|
||||
) {
|
||||
const viewId = this.getViewIdFromFieldsWidget(widget);
|
||||
|
||||
if (isDefined(viewId)) {
|
||||
viewIdsToDelete.add(viewId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out viewIds still referenced by surviving widgets
|
||||
for (const widget of allExistingWidgets) {
|
||||
if (
|
||||
!isDefined(widget.deletedAt) &&
|
||||
!directlyDeletedWidgetIds.has(widget.id) &&
|
||||
!deletedTabIds.has(widget.pageLayoutTabId)
|
||||
) {
|
||||
const viewId = this.getViewIdFromFieldsWidget(widget);
|
||||
|
||||
if (isDefined(viewId)) {
|
||||
viewIdsToDelete.delete(viewId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...viewIdsToDelete];
|
||||
}
|
||||
|
||||
private getViewIdFromFieldsWidget(
|
||||
widget: FlatPageLayoutWidget,
|
||||
): string | undefined {
|
||||
if (
|
||||
widget.configuration.configurationType !== WidgetConfigurationType.FIELDS
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const viewId = (widget.configuration as { viewId?: string | null }).viewId;
|
||||
|
||||
return typeof viewId === 'string' ? viewId : undefined;
|
||||
}
|
||||
|
||||
private async destroyOrphanedFieldsWidgetViews({
|
||||
viewIds,
|
||||
workspaceId,
|
||||
}: {
|
||||
viewIds: string[];
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
for (const viewId of viewIds) {
|
||||
try {
|
||||
await this.viewService.destroyOne({
|
||||
destroyViewInput: { id: viewId },
|
||||
workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to destroy view ${viewId} after Fields widget deletion: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import { ApplicationService } from 'src/engine/core-modules/application/applicat
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.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 { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
@@ -31,9 +31,12 @@ import { DestroyViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/d
|
||||
import { UpdateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/update-view.input';
|
||||
import { ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { computeFieldsWidgetViewFieldsAndGroupsToCreate } from 'src/engine/metadata-modules/view/utils/compute-fields-widget-view-fields-and-groups-to-create.util';
|
||||
import { fromFlatViewToViewDto } from 'src/engine/metadata-modules/view/utils/from-flat-view-to-view-dto.util';
|
||||
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 UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type';
|
||||
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
|
||||
|
||||
@Injectable()
|
||||
export class ViewService {
|
||||
@@ -82,6 +85,39 @@ export class ViewService {
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
let flatViewFieldGroupsToCreate: UniversalFlatViewFieldGroup[] = [];
|
||||
let flatViewFieldsToCreate: UniversalFlatViewField[] = [];
|
||||
|
||||
if (flatViewToCreate.type === ViewType.FIELDS_WIDGET) {
|
||||
const objectFlatFieldMetadatas = Object.values(
|
||||
existingFlatFieldMetadataMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(field): field is NonNullable<typeof field> =>
|
||||
field !== undefined &&
|
||||
field.objectMetadataUniversalIdentifier ===
|
||||
flatViewToCreate.objectMetadataUniversalIdentifier,
|
||||
);
|
||||
|
||||
const objectFlatMetadata = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: existingFlatObjectMetadataMaps,
|
||||
universalIdentifier: flatViewToCreate.objectMetadataUniversalIdentifier,
|
||||
});
|
||||
|
||||
const fieldsWidgetResult = computeFieldsWidgetViewFieldsAndGroupsToCreate(
|
||||
{
|
||||
objectFlatFieldMetadatas,
|
||||
viewUniversalIdentifier: flatViewToCreate.universalIdentifier,
|
||||
flatApplication: workspaceCustomFlatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
objectFlatMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
flatViewFieldGroupsToCreate =
|
||||
fieldsWidgetResult.flatViewFieldGroupsToCreate;
|
||||
flatViewFieldsToCreate = fieldsWidgetResult.flatViewFieldsToCreate;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
@@ -97,6 +133,18 @@ export class ViewService {
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
|
||||
viewFieldGroup: {
|
||||
flatEntityToCreate: flatViewFieldGroupsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
|
||||
viewField: {
|
||||
flatEntityToCreate: flatViewFieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
|
||||
+415
@@ -0,0 +1,415 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
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 { computeFieldsWidgetViewFieldsAndGroupsToCreate } from '../compute-fields-widget-view-fields-and-groups-to-create.util';
|
||||
|
||||
const makeFieldMetadata = (
|
||||
overrides: Partial<UniversalFlatFieldMetadata> & {
|
||||
name: string;
|
||||
type: FieldMetadataType;
|
||||
},
|
||||
): UniversalFlatFieldMetadata => {
|
||||
const universalIdentifier = overrides.universalIdentifier ?? v4();
|
||||
|
||||
return {
|
||||
universalIdentifier,
|
||||
objectMetadataUniversalIdentifier: 'object-uid',
|
||||
applicationUniversalIdentifier: 'app-uid',
|
||||
name: overrides.name,
|
||||
label: overrides.label ?? overrides.name,
|
||||
type: overrides.type,
|
||||
isCustom: overrides.isCustom ?? false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isUIReadOnly: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: false,
|
||||
defaultValue: null,
|
||||
description: null,
|
||||
icon: null,
|
||||
options: null,
|
||||
settings: null,
|
||||
standardOverrides: null,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
deletedAt: null,
|
||||
} as unknown as UniversalFlatFieldMetadata;
|
||||
};
|
||||
|
||||
const flatApplication = {
|
||||
universalIdentifier: 'app-uid',
|
||||
id: 'app-id',
|
||||
} as never;
|
||||
|
||||
const viewUniversalIdentifier = 'view-uid';
|
||||
|
||||
describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
|
||||
it('should create a "General" group with standard fields', () => {
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: false,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
isCustom: false,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
expect(result.flatViewFieldGroupsToCreate).toHaveLength(1);
|
||||
expect(result.flatViewFieldGroupsToCreate[0].name).toBe('General');
|
||||
expect(result.flatViewFieldGroupsToCreate[0].position).toBe(0);
|
||||
expect(result.flatViewFieldGroupsToCreate[0].isVisible).toBe(true);
|
||||
|
||||
expect(result.flatViewFieldsToCreate).toHaveLength(2);
|
||||
result.flatViewFieldsToCreate.forEach((vf) => {
|
||||
expect(vf.viewFieldGroupUniversalIdentifier).toBe(
|
||||
result.flatViewFieldGroupsToCreate[0].universalIdentifier,
|
||||
);
|
||||
expect(vf.isVisible).toBe(true);
|
||||
expect(vf.size).toBe(DEFAULT_VIEW_FIELD_SIZE);
|
||||
expect(vf.viewUniversalIdentifier).toBe(viewUniversalIdentifier);
|
||||
});
|
||||
|
||||
expect(result.flatViewFieldsToCreate[0].position).toBe(0);
|
||||
expect(result.flatViewFieldsToCreate[1].position).toBe(1);
|
||||
});
|
||||
|
||||
it('should create an "Other" group when custom fields exist', () => {
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: false,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'customRating',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
isCustom: true,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'customTag',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: true,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
expect(result.flatViewFieldGroupsToCreate).toHaveLength(2);
|
||||
expect(result.flatViewFieldGroupsToCreate[0].name).toBe('General');
|
||||
expect(result.flatViewFieldGroupsToCreate[0].position).toBe(0);
|
||||
expect(result.flatViewFieldGroupsToCreate[1].name).toBe('Other');
|
||||
expect(result.flatViewFieldGroupsToCreate[1].position).toBe(1);
|
||||
|
||||
const generalGroupUid =
|
||||
result.flatViewFieldGroupsToCreate[0].universalIdentifier;
|
||||
const otherGroupUid =
|
||||
result.flatViewFieldGroupsToCreate[1].universalIdentifier;
|
||||
|
||||
const generalFields = result.flatViewFieldsToCreate.filter(
|
||||
(vf) => vf.viewFieldGroupUniversalIdentifier === generalGroupUid,
|
||||
);
|
||||
const otherFields = result.flatViewFieldsToCreate.filter(
|
||||
(vf) => vf.viewFieldGroupUniversalIdentifier === otherGroupUid,
|
||||
);
|
||||
|
||||
expect(generalFields).toHaveLength(1);
|
||||
expect(otherFields).toHaveLength(2);
|
||||
|
||||
// Positions are sequential within each group
|
||||
expect(otherFields[0].position).toBe(0);
|
||||
expect(otherFields[1].position).toBe(1);
|
||||
});
|
||||
|
||||
it('should not create an "Other" group when there are no custom fields', () => {
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: false,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
expect(result.flatViewFieldGroupsToCreate).toHaveLength(1);
|
||||
expect(result.flatViewFieldGroupsToCreate[0].name).toBe('General');
|
||||
});
|
||||
|
||||
it('should exclude deletedAt field', () => {
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: false,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
isCustom: false,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
expect(result.flatViewFieldsToCreate).toHaveLength(1);
|
||||
expect(
|
||||
result.flatViewFieldsToCreate[0].fieldMetadataUniversalIdentifier,
|
||||
).toBe(fields[0].universalIdentifier);
|
||||
});
|
||||
|
||||
it('should exclude TS_VECTOR and POSITION fields', () => {
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: false,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
isCustom: false,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
isCustom: false,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
expect(result.flatViewFieldsToCreate).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should exclude id field unless it is the label identifier', () => {
|
||||
const idUid = v4();
|
||||
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
isCustom: false,
|
||||
universalIdentifier: idUid,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: false,
|
||||
}),
|
||||
];
|
||||
|
||||
// Without label identifier match: id excluded
|
||||
const resultWithout = computeFieldsWidgetViewFieldsAndGroupsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
expect(resultWithout.flatViewFieldsToCreate).toHaveLength(1);
|
||||
|
||||
// With label identifier match: id included
|
||||
const resultWith = computeFieldsWidgetViewFieldsAndGroupsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: idUid,
|
||||
});
|
||||
|
||||
expect(resultWith.flatViewFieldsToCreate).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should set correct applicationUniversalIdentifier on all entities', () => {
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: false,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'custom',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: true,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
result.flatViewFieldGroupsToCreate.forEach((group) => {
|
||||
expect(group.applicationUniversalIdentifier).toBe('app-uid');
|
||||
});
|
||||
result.flatViewFieldsToCreate.forEach((field) => {
|
||||
expect(field.applicationUniversalIdentifier).toBe('app-uid');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty fields when all fields are excluded', () => {
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
isCustom: false,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
isCustom: false,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
// Still creates the "General" group, just with no fields
|
||||
expect(result.flatViewFieldGroupsToCreate).toHaveLength(1);
|
||||
expect(result.flatViewFieldsToCreate).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should place label identifier field at position 0', () => {
|
||||
const labelUid = v4();
|
||||
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
isCustom: false,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
isCustom: false,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: false,
|
||||
universalIdentifier: labelUid,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: labelUid,
|
||||
});
|
||||
|
||||
expect(result.flatViewFieldsToCreate).toHaveLength(3);
|
||||
|
||||
// The label identifier field must be at the lowest position
|
||||
const labelField = result.flatViewFieldsToCreate.find(
|
||||
(vf) => vf.fieldMetadataUniversalIdentifier === labelUid,
|
||||
);
|
||||
|
||||
expect(labelField).toBeDefined();
|
||||
expect(labelField!.position).toBe(0);
|
||||
|
||||
// Other fields should have higher positions
|
||||
const otherPositions = result.flatViewFieldsToCreate
|
||||
.filter((vf) => vf.fieldMetadataUniversalIdentifier !== labelUid)
|
||||
.map((vf) => vf.position);
|
||||
|
||||
otherPositions.forEach((pos) => {
|
||||
expect(pos).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should make RELATION and MORPH_RELATION fields invisible by default', () => {
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: false,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
isCustom: false,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'target',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
isCustom: false,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'customRelation',
|
||||
type: FieldMetadataType.RELATION,
|
||||
isCustom: true,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
const viewFieldForName = result.flatViewFieldsToCreate.find(
|
||||
(vf) =>
|
||||
vf.fieldMetadataUniversalIdentifier === fields[0].universalIdentifier,
|
||||
);
|
||||
const viewFieldForCompany = result.flatViewFieldsToCreate.find(
|
||||
(vf) =>
|
||||
vf.fieldMetadataUniversalIdentifier === fields[1].universalIdentifier,
|
||||
);
|
||||
const viewFieldForTarget = result.flatViewFieldsToCreate.find(
|
||||
(vf) =>
|
||||
vf.fieldMetadataUniversalIdentifier === fields[2].universalIdentifier,
|
||||
);
|
||||
const viewFieldForCustomRelation = result.flatViewFieldsToCreate.find(
|
||||
(vf) =>
|
||||
vf.fieldMetadataUniversalIdentifier === fields[3].universalIdentifier,
|
||||
);
|
||||
|
||||
expect(viewFieldForName!.isVisible).toBe(true);
|
||||
expect(viewFieldForCompany!.isVisible).toBe(false);
|
||||
expect(viewFieldForTarget!.isVisible).toBe(false);
|
||||
expect(viewFieldForCustomRelation!.isVisible).toBe(false);
|
||||
});
|
||||
});
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
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 UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type';
|
||||
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
|
||||
|
||||
export const computeFieldsWidgetViewFieldsAndGroupsToCreate = ({
|
||||
objectFlatFieldMetadatas,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
}: {
|
||||
objectFlatFieldMetadatas: UniversalFlatFieldMetadata[];
|
||||
viewUniversalIdentifier: string;
|
||||
flatApplication: FlatApplication;
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
|
||||
}): {
|
||||
flatViewFieldGroupsToCreate: UniversalFlatViewFieldGroup[];
|
||||
flatViewFieldsToCreate: UniversalFlatViewField[];
|
||||
} => {
|
||||
const createdAt = new Date().toISOString();
|
||||
const applicationUniversalIdentifier = flatApplication.universalIdentifier;
|
||||
|
||||
const eligibleFields = objectFlatFieldMetadatas.filter(
|
||||
(field) =>
|
||||
field.name !== 'deletedAt' &&
|
||||
field.type !== FieldMetadataType.TS_VECTOR &&
|
||||
field.type !== FieldMetadataType.POSITION &&
|
||||
(field.name !== 'id' ||
|
||||
field.universalIdentifier ===
|
||||
labelIdentifierFieldMetadataUniversalIdentifier),
|
||||
);
|
||||
|
||||
const standardFields = eligibleFields.filter((field) => !field.isCustom);
|
||||
const customFields = eligibleFields.filter((field) => field.isCustom);
|
||||
|
||||
const sortedStandardFields = [...standardFields].sort((a, b) => {
|
||||
const aIsLabel =
|
||||
a.universalIdentifier === labelIdentifierFieldMetadataUniversalIdentifier;
|
||||
const bIsLabel =
|
||||
b.universalIdentifier === labelIdentifierFieldMetadataUniversalIdentifier;
|
||||
|
||||
if (aIsLabel && !bIsLabel) return -1;
|
||||
if (!aIsLabel && bIsLabel) return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const flatViewFieldGroupsToCreate: UniversalFlatViewFieldGroup[] = [];
|
||||
const flatViewFieldsToCreate: UniversalFlatViewField[] = [];
|
||||
|
||||
const generalGroupUniversalIdentifier = v4();
|
||||
|
||||
flatViewFieldGroupsToCreate.push({
|
||||
universalIdentifier: generalGroupUniversalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
name: 'General',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
overrides: null,
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
sortedStandardFields.forEach((field, index) => {
|
||||
const isVisible =
|
||||
field.type !== FieldMetadataType.RELATION &&
|
||||
field.type !== FieldMetadataType.MORPH_RELATION;
|
||||
|
||||
flatViewFieldsToCreate.push({
|
||||
fieldMetadataUniversalIdentifier: field.universalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
viewFieldGroupUniversalIdentifier: generalGroupUniversalIdentifier,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
universalIdentifier: v4(),
|
||||
isVisible,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
position: index,
|
||||
aggregateOperation: null,
|
||||
universalOverrides: null,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
});
|
||||
|
||||
if (customFields.length > 0) {
|
||||
const otherGroupUniversalIdentifier = v4();
|
||||
|
||||
flatViewFieldGroupsToCreate.push({
|
||||
universalIdentifier: otherGroupUniversalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
name: 'Other',
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
overrides: null,
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
customFields.forEach((field, index) => {
|
||||
const isVisible =
|
||||
field.type !== FieldMetadataType.RELATION &&
|
||||
field.type !== FieldMetadataType.MORPH_RELATION;
|
||||
|
||||
flatViewFieldsToCreate.push({
|
||||
fieldMetadataUniversalIdentifier: field.universalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
viewFieldGroupUniversalIdentifier: otherGroupUniversalIdentifier,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
universalIdentifier: v4(),
|
||||
isVisible,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
position: index,
|
||||
aggregateOperation: null,
|
||||
universalOverrides: null,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
flatViewFieldGroupsToCreate,
|
||||
flatViewFieldsToCreate,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user