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
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should match snapshot 1`] = `
{
@@ -246,6 +246,16 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"size",
"position",
"aggregateOperation",
"viewFieldGroupUniversalIdentifier",
"deletedAt",
],
"propertiesToStringify": [],
},
"viewFieldGroup": {
"propertiesToCompare": [
"name",
"position",
"isVisible",
"deletedAt",
],
"propertiesToStringify": [],
@@ -112,6 +112,16 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
size: { toStringify: false, universalProperty: undefined },
position: { toStringify: false, universalProperty: undefined },
aggregateOperation: { toStringify: false, universalProperty: undefined },
viewFieldGroupId: {
toStringify: false,
universalProperty: 'viewFieldGroupUniversalIdentifier',
},
deletedAt: { toStringify: false, universalProperty: undefined },
},
viewFieldGroup: {
name: { toStringify: false, universalProperty: undefined },
position: { toStringify: false, universalProperty: undefined },
isVisible: { toStringify: false, universalProperty: undefined },
deletedAt: { toStringify: false, universalProperty: undefined },
},
viewGroup: {
@@ -18,6 +18,7 @@ import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modu
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.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';
@@ -26,6 +27,7 @@ import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entit
export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
viewField: ViewFieldEntity,
viewFieldGroup: ViewFieldGroupEntity,
viewFilter: ViewFilterEntity,
viewGroup: ViewGroupEntity,
viewFilterGroup: ViewFilterGroupEntity,
@@ -221,6 +221,7 @@ export const ALL_METADATA_RELATIONS = {
metadataName: 'viewFilterGroup',
},
viewGroups: { metadataName: 'viewGroup' },
viewFieldGroups: { metadataName: 'viewFieldGroup' },
// @ts-expect-error TODO migrate viewSort to v2
viewSorts: null,
},
@@ -239,11 +240,32 @@ export const ALL_METADATA_RELATIONS = {
foreignKey: 'viewId',
isNullable: false,
},
viewFieldGroup: {
metadataName: 'viewFieldGroup',
flatEntityForeignKeyAggregator: 'viewFieldIds',
foreignKey: 'viewFieldGroupId',
isNullable: true,
},
workspace: null,
application: null,
},
oneToMany: {},
},
viewFieldGroup: {
manyToOne: {
view: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFieldGroupIds',
foreignKey: 'viewId',
isNullable: false,
},
workspace: null,
application: null,
},
oneToMany: {
viewFields: { metadataName: 'viewField' },
},
},
viewFilter: {
manyToOne: {
fieldMetadata: {
@@ -27,6 +27,10 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
view: true,
fieldMetadata: true,
objectMetadata: true,
viewFieldGroup: true,
},
viewFieldGroup: {
view: true,
},
index: {
objectMetadata: true,