Fallback to field metadata (#19131)
Rely on the field metadata items to always display all object's fields in the fields widget configuration editor. If fields are missing in the returned view fields, we add the missing fields through object metadata. https://github.com/user-attachments/assets/3c4d45e8-05d0-4943-be4b-bcf1e310155c
This commit is contained in:
committed by
GitHub
parent
5bbfce7789
commit
c11e4ece39
+19
-4
@@ -1,15 +1,30 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsBoolean, IsNotEmpty, IsNumber, IsUUID } from 'class-validator';
|
||||
import { IsBoolean, IsNumber, IsOptional, IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { AtLeastOneOf } from 'src/engine/metadata-modules/view-field-group/dtos/validators/at-least-one-of.validator';
|
||||
|
||||
@InputType()
|
||||
@AtLeastOneOf(['viewFieldId', 'fieldMetadataId'])
|
||||
export class UpsertFieldsWidgetFieldInput {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType, { description: 'The id of the view field' })
|
||||
viewFieldId: string;
|
||||
@Field(() => UUIDScalarType, {
|
||||
nullable: true,
|
||||
description:
|
||||
'The id of the view field. Required if fieldMetadataId is not provided.',
|
||||
})
|
||||
viewFieldId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, {
|
||||
nullable: true,
|
||||
description:
|
||||
'The id of the field metadata. Used to create a new view field when viewFieldId is not provided.',
|
||||
})
|
||||
fieldMetadataId?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
type ValidationArguments,
|
||||
type ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
type ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@ValidatorConstraint({ async: false })
|
||||
export class AtLeastOneOfConstraint implements ValidatorConstraintInterface {
|
||||
validate(_value: unknown, args: ValidationArguments) {
|
||||
const [properties] = args.constraints as [string[]];
|
||||
const object = args.object as Record<string, unknown>;
|
||||
|
||||
return properties.some((property) => isDefined(object[property]));
|
||||
}
|
||||
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
const [properties] = args.constraints as [string[]];
|
||||
|
||||
return `At least one of the following properties must be provided: ${properties.join(', ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const AtLeastOneOf = (
|
||||
properties: string[],
|
||||
validationOptions?: ValidationOptions,
|
||||
): ClassDecorator => {
|
||||
return (target) => {
|
||||
registerDecorator({
|
||||
target,
|
||||
propertyName: properties[0],
|
||||
options: validationOptions,
|
||||
constraints: [properties],
|
||||
validator: AtLeastOneOfConstraint,
|
||||
});
|
||||
};
|
||||
};
|
||||
+3
-1
@@ -1,4 +1,4 @@
|
||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
Context,
|
||||
@@ -13,6 +13,7 @@ import { isArray } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
@@ -148,6 +149,7 @@ export class ViewFieldGroupResolver {
|
||||
|
||||
@Mutation(() => ViewDTO)
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
async upsertFieldsWidget(
|
||||
@Args('input') input: UpsertFieldsWidgetInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
|
||||
+185
-3
@@ -2,17 +2,25 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
isDefined,
|
||||
isFieldMetadataEligibleForFieldsWidget,
|
||||
} from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/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 { 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';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { isFlatPageLayoutWidgetConfigurationOfType } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/is-flat-page-layout-widget-configuration-of-type.util';
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
|
||||
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant';
|
||||
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
|
||||
import { fromViewFieldOverridesToUniversalOverrides } from 'src/engine/metadata-modules/flat-view-field/utils/from-view-field-overrides-to-universal-overrides.util';
|
||||
import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type';
|
||||
@@ -66,6 +74,8 @@ export class FieldsWidgetUpsertService {
|
||||
|
||||
const {
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
flatViewMaps,
|
||||
@@ -75,6 +85,8 @@ export class FieldsWidgetUpsertService {
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
'flatViewFieldMaps',
|
||||
'flatViewMaps',
|
||||
@@ -109,6 +121,22 @@ export class FieldsWidgetUpsertService {
|
||||
);
|
||||
}
|
||||
|
||||
const flatView = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: viewId,
|
||||
flatEntityMaps: flatViewMaps,
|
||||
});
|
||||
|
||||
const objectMetadata = isDefined(flatView)
|
||||
? findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatView.objectMetadataId,
|
||||
flatEntityMaps:
|
||||
flatObjectMetadataMaps as FlatEntityMaps<FlatObjectMetadata>,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const labelIdentifierFieldMetadataId =
|
||||
objectMetadata?.labelIdentifierFieldMetadataId ?? null;
|
||||
|
||||
const existingGroups = Object.values(
|
||||
flatViewFieldGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
@@ -135,6 +163,8 @@ export class FieldsWidgetUpsertService {
|
||||
applicationId: workspaceCustomFlatApplication.id,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
labelIdentifierFieldMetadataId,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
});
|
||||
@@ -143,9 +173,14 @@ export class FieldsWidgetUpsertService {
|
||||
inputFields: input.fields!,
|
||||
existingGroups,
|
||||
existingViewFields,
|
||||
viewId,
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomFlatApplication.id,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
labelIdentifierFieldMetadataId,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,6 +206,8 @@ export class FieldsWidgetUpsertService {
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataId,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
}: {
|
||||
@@ -181,6 +218,8 @@ export class FieldsWidgetUpsertService {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
labelIdentifierFieldMetadataId: string | null;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatViewMaps: FlatViewMaps;
|
||||
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
|
||||
}): Promise<void> {
|
||||
@@ -362,6 +401,77 @@ export class FieldsWidgetUpsertService {
|
||||
return [updatedField];
|
||||
});
|
||||
|
||||
const viewFieldsToCreate: FlatViewField[] = [];
|
||||
|
||||
for (const inputGroup of inputGroups) {
|
||||
for (const inputField of inputGroup.fields) {
|
||||
if (
|
||||
isDefined(inputField.viewFieldId) ||
|
||||
!isDefined(inputField.fieldMetadataId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: inputField.fieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(fieldMetadata) ||
|
||||
!isFieldMetadataEligibleForFieldsWidget({
|
||||
fieldName: fieldMetadata.name,
|
||||
fieldType: fieldMetadata.type,
|
||||
isLabelIdentifierField:
|
||||
fieldMetadata.id === labelIdentifierFieldMetadataId,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const {
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
viewFieldGroupUniversalIdentifier,
|
||||
} = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewField',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId: inputField.fieldMetadataId,
|
||||
viewId,
|
||||
viewFieldGroupId: inputGroup.id,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldGroupMaps: optimisticFlatViewFieldGroupMaps,
|
||||
},
|
||||
});
|
||||
|
||||
viewFieldsToCreate.push({
|
||||
id: v4(),
|
||||
workspaceId,
|
||||
applicationId,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
fieldMetadataId: inputField.fieldMetadataId,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewId,
|
||||
viewUniversalIdentifier,
|
||||
viewFieldGroupId: inputGroup.id,
|
||||
viewFieldGroupUniversalIdentifier,
|
||||
isVisible: inputField.isVisible,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
position: inputField.position,
|
||||
aggregateOperation: null,
|
||||
overrides: null,
|
||||
universalOverrides: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const fieldsWithStaleGroupOverrides =
|
||||
this.buildFieldUpdatesForStaleGroupOverrides({
|
||||
existingViewFields,
|
||||
@@ -382,7 +492,7 @@ export class FieldsWidgetUpsertService {
|
||||
flatEntityToUpdate: groupsToUpdate,
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToCreate: viewFieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [
|
||||
...viewFieldsToUpdate,
|
||||
@@ -408,14 +518,24 @@ export class FieldsWidgetUpsertService {
|
||||
inputFields,
|
||||
existingGroups,
|
||||
existingViewFields,
|
||||
viewId,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataId,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
}: {
|
||||
inputFields: UpsertFieldsWidgetFieldInput[];
|
||||
existingGroups: FlatViewFieldGroup[];
|
||||
existingViewFields: FlatViewField[];
|
||||
viewId: string;
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
labelIdentifierFieldMetadataId: string | null;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatViewMaps: FlatViewMaps;
|
||||
}): Promise<void> {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
@@ -494,6 +614,68 @@ export class FieldsWidgetUpsertService {
|
||||
return [updatedField];
|
||||
});
|
||||
|
||||
const viewFieldsToCreate: FlatViewField[] = inputFields
|
||||
.filter((inputField) => {
|
||||
if (
|
||||
isDefined(inputField.viewFieldId) ||
|
||||
!isDefined(inputField.fieldMetadataId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fieldMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: inputField.fieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
return (
|
||||
isDefined(fieldMetadata) &&
|
||||
isFieldMetadataEligibleForFieldsWidget({
|
||||
fieldName: fieldMetadata.name,
|
||||
fieldType: fieldMetadata.type,
|
||||
isLabelIdentifierField:
|
||||
fieldMetadata.id === labelIdentifierFieldMetadataId,
|
||||
})
|
||||
);
|
||||
})
|
||||
.map((inputField) => {
|
||||
const { fieldMetadataUniversalIdentifier, viewUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewField',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId: inputField.fieldMetadataId!,
|
||||
viewId,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
workspaceId,
|
||||
applicationId,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
fieldMetadataId: inputField.fieldMetadataId!,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewId,
|
||||
viewUniversalIdentifier,
|
||||
viewFieldGroupId: null,
|
||||
viewFieldGroupUniversalIdentifier: null,
|
||||
isVisible: inputField.isVisible,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
position: inputField.position,
|
||||
aggregateOperation: null,
|
||||
overrides: null,
|
||||
universalOverrides: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
};
|
||||
});
|
||||
|
||||
const fieldsWithStaleGroupOverrides =
|
||||
this.buildFieldUpdatesForStaleGroupOverrides({
|
||||
existingViewFields,
|
||||
@@ -514,7 +696,7 @@ export class FieldsWidgetUpsertService {
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToCreate: viewFieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [
|
||||
...viewFieldsToUpdate,
|
||||
|
||||
Reference in New Issue
Block a user