Introducing view field group syncable entity (#17867)
## Context Introduces a new viewFieldGroup entity that allows grouping view fields into sections (e.g. "General", "Additional", "Other") within a view. The page layout fields widget needs a way to organize fields into sections. Today, views have no concept of field grouping. This PR introduces the viewFieldGroup entity which sits between a view and its viewFields, enabling section-based organization. <img width="401" height="724" alt="Layout - V2 (customize visibility)" src="https://github.com/user-attachments/assets/6376e2ab-44db-42bf-9d2c-758f56f6b548" /> --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
|
||||
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
|
||||
import { ViewFilterGroupDTO } from 'src/engine/metadata-modules/view-filter-group/dtos/view-filter-group.dto';
|
||||
import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto';
|
||||
@@ -104,6 +105,9 @@ export class ViewDTO {
|
||||
@Field(() => [ViewGroupDTO])
|
||||
viewGroups?: ViewGroupDTO[];
|
||||
|
||||
@Field(() => [ViewFieldGroupDTO])
|
||||
viewFieldGroups?: ViewFieldGroupDTO[];
|
||||
|
||||
@Field(() => ViewVisibility, {
|
||||
nullable: false,
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
|
||||
@@ -191,6 +192,12 @@ export class ViewEntity extends SyncableEntity implements Required<ViewEntity> {
|
||||
@OneToMany(() => ViewFieldEntity, (viewField) => viewField.view)
|
||||
viewFields: Relation<ViewFieldEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => ViewFieldGroupEntity,
|
||||
(viewFieldGroup) => viewFieldGroup.view,
|
||||
)
|
||||
viewFieldGroups: Relation<ViewFieldGroupEntity[]>;
|
||||
|
||||
@OneToMany(() => ViewFilterEntity, (viewFilter) => viewFilter.view)
|
||||
viewFilters: Relation<ViewFilterEntity[]>;
|
||||
|
||||
|
||||
@@ -11,19 +11,20 @@ import {
|
||||
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 { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
|
||||
import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
|
||||
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
|
||||
import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { ViewFilterGroupDTO } from 'src/engine/metadata-modules/view-filter-group/dtos/view-filter-group.dto';
|
||||
import { ViewFilterGroupService } from 'src/engine/metadata-modules/view-filter-group/services/view-filter-group.service';
|
||||
import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto';
|
||||
@@ -56,7 +57,7 @@ export class ViewResolver {
|
||||
private readonly viewGroupService: ViewGroupService,
|
||||
private readonly i18nService: I18nService,
|
||||
|
||||
private readonly viewFieldV2Service: ViewFieldV2Service,
|
||||
private readonly viewFieldService: ViewFieldService,
|
||||
) {}
|
||||
|
||||
@ResolveField(() => String)
|
||||
@@ -209,7 +210,7 @@ export class ViewResolver {
|
||||
return view.viewFields;
|
||||
}
|
||||
|
||||
return this.viewFieldV2Service.findByViewId(workspace.id, view.id);
|
||||
return this.viewFieldService.findByViewId(workspace.id, view.id);
|
||||
}
|
||||
|
||||
@ResolveField(() => [ViewFilterDTO])
|
||||
@@ -259,4 +260,20 @@ export class ViewResolver {
|
||||
|
||||
return this.viewGroupService.findByViewId(workspace.id, view.id);
|
||||
}
|
||||
|
||||
@ResolveField(() => [ViewFieldGroupDTO])
|
||||
async viewFieldGroups(
|
||||
@Parent() view: ViewDTO,
|
||||
@Context() context: { loaders: IDataloaders },
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
) {
|
||||
if (isArray(view.viewFieldGroups)) {
|
||||
return view.viewFieldGroups;
|
||||
}
|
||||
|
||||
return context.loaders.viewFieldGroupsByViewIdLoader.load({
|
||||
workspaceId: workspace.id,
|
||||
viewId: view.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -7,6 +7,10 @@ import {
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
ViewFieldGroupException,
|
||||
ViewFieldGroupExceptionCode,
|
||||
} from 'src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception';
|
||||
import {
|
||||
ViewFieldException,
|
||||
ViewFieldExceptionCode,
|
||||
@@ -77,6 +81,22 @@ export const viewGraphqlApiExceptionHandler = (error: Error, i18n: I18n) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof ViewFieldGroupException) {
|
||||
switch (error.code) {
|
||||
case ViewFieldGroupExceptionCode.VIEW_FIELD_GROUP_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
case ViewFieldGroupExceptionCode.VIEW_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
case ViewFieldGroupExceptionCode.INVALID_VIEW_FIELD_GROUP_DATA:
|
||||
throw new UserInputError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof ViewFilterException) {
|
||||
switch (error.code) {
|
||||
case ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND:
|
||||
|
||||
+2
@@ -9,6 +9,7 @@ import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { ViewFieldGroupException } from 'src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception';
|
||||
import { ViewFieldException } from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
|
||||
import { ViewFilterGroupException } from 'src/engine/metadata-modules/view-filter-group/exceptions/view-filter-group.exception';
|
||||
import { ViewFilterException } from 'src/engine/metadata-modules/view-filter/exceptions/view-filter.exception';
|
||||
@@ -35,6 +36,7 @@ export class ViewGraphqlApiExceptionFilter implements ExceptionFilter {
|
||||
exception:
|
||||
| ViewException
|
||||
| ViewFieldException
|
||||
| ViewFieldGroupException
|
||||
| ViewFilterException
|
||||
| ViewFilterGroupException
|
||||
| ViewGroupException
|
||||
|
||||
@@ -7,6 +7,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata
|
||||
import { FlatViewModule } from 'src/engine/metadata-modules/flat-view/flat-view.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { ViewFieldGroupModule } from 'src/engine/metadata-modules/view-field-group/view-field-group.module';
|
||||
import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module';
|
||||
import { ViewFilterGroupModule } from 'src/engine/metadata-modules/view-filter-group/view-filter-group.module';
|
||||
import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module';
|
||||
@@ -27,6 +28,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
TypeOrmModule.forFeature([ViewEntity]),
|
||||
ViewPermissionsModule,
|
||||
ViewFieldModule,
|
||||
ViewFieldGroupModule,
|
||||
ViewFilterModule,
|
||||
ViewFilterGroupModule,
|
||||
ViewGroupModule,
|
||||
|
||||
Reference in New Issue
Block a user