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:
Weiko
2026-02-11 22:02:58 +01:00
committed by GitHub
parent ed66fbd71b
commit 6aca1dd013
126 changed files with 3593 additions and 115 deletions
@@ -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,
});
}
}