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:
+1
@@ -5,4 +5,5 @@ export const FLAT_VIEW_FIELD_EDITABLE_PROPERTIES = [
|
||||
'size',
|
||||
'position',
|
||||
'aggregateOperation',
|
||||
'viewFieldGroupId',
|
||||
] as const satisfies MetadataEntityPropertyName<'viewField'>[];
|
||||
|
||||
+14
-4
@@ -10,6 +10,7 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
|
||||
import { fromViewFieldEntityToFlatViewField } from 'src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util';
|
||||
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 { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
@@ -28,13 +29,15 @@ export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvide
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectRepository(ViewEntity)
|
||||
private readonly viewRepository: Repository<ViewEntity>,
|
||||
@InjectRepository(ViewFieldGroupEntity)
|
||||
private readonly viewFieldGroupRepository: Repository<ViewFieldGroupEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(workspaceId: string): Promise<FlatViewFieldMaps> {
|
||||
const [viewFields, applications, fieldMetadatas, views] = await Promise.all(
|
||||
[
|
||||
const [viewFields, applications, fieldMetadatas, views, viewFieldGroups] =
|
||||
await Promise.all([
|
||||
this.viewFieldRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
@@ -54,8 +57,12 @@ export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvide
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
],
|
||||
);
|
||||
this.viewFieldGroupRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const applicationIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(applications);
|
||||
@@ -63,6 +70,8 @@ export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvide
|
||||
createIdToUniversalIdentifierMap(fieldMetadatas);
|
||||
const viewIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(views);
|
||||
const viewFieldGroupIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(viewFieldGroups);
|
||||
|
||||
const flatViewFieldMaps = createEmptyFlatEntityMaps();
|
||||
|
||||
@@ -72,6 +81,7 @@ export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvide
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
fieldMetadataIdToUniversalIdentifierMap,
|
||||
viewIdToUniversalIdentifierMap,
|
||||
viewFieldGroupIdToUniversalIdentifierMap,
|
||||
});
|
||||
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
|
||||
+27
-9
@@ -11,31 +11,48 @@ import { type UniversalFlatViewField } from 'src/engine/workspace-manager/worksp
|
||||
export type FromCreateViewFieldInputToFlatViewFieldToCreateArgs = {
|
||||
createViewFieldInput: CreateViewFieldInput;
|
||||
flatApplication: FlatApplication;
|
||||
} & Pick<AllFlatEntityMaps, 'flatFieldMetadataMaps' | 'flatViewMaps'>;
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatFieldMetadataMaps' | 'flatViewMaps' | 'flatViewFieldGroupMaps'
|
||||
>;
|
||||
|
||||
export const fromCreateViewFieldInputToFlatViewFieldToCreate = ({
|
||||
createViewFieldInput: rawCreateViewFieldInput,
|
||||
flatApplication,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
}: FromCreateViewFieldInputToFlatViewFieldToCreateArgs): UniversalFlatViewField & {
|
||||
id: string;
|
||||
} => {
|
||||
const { fieldMetadataId, viewId, ...createViewFieldInput } =
|
||||
const { fieldMetadataId, viewId, viewFieldGroupId, ...createViewFieldInput } =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
rawCreateViewFieldInput,
|
||||
['aggregateOperation', 'fieldMetadataId', 'id', 'viewId'],
|
||||
[
|
||||
'aggregateOperation',
|
||||
'fieldMetadataId',
|
||||
'id',
|
||||
'viewId',
|
||||
'viewFieldGroupId',
|
||||
],
|
||||
);
|
||||
|
||||
const createdAt = new Date().toISOString();
|
||||
const viewFieldId = createViewFieldInput.id ?? v4();
|
||||
|
||||
const { fieldMetadataUniversalIdentifier, viewUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewField',
|
||||
foreignKeyValues: { fieldMetadataId, viewId },
|
||||
flatEntityMaps: { flatFieldMetadataMaps, flatViewMaps },
|
||||
});
|
||||
const {
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
viewFieldGroupUniversalIdentifier,
|
||||
} = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewField',
|
||||
foreignKeyValues: { fieldMetadataId, viewId, viewFieldGroupId },
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: viewFieldId,
|
||||
@@ -49,6 +66,7 @@ export const fromCreateViewFieldInputToFlatViewFieldToCreate = ({
|
||||
size: createViewFieldInput.size ?? DEFAULT_VIEW_FIELD_SIZE,
|
||||
position: createViewFieldInput.position ?? 0,
|
||||
aggregateOperation: createViewFieldInput.aggregateOperation ?? null,
|
||||
viewFieldGroupUniversalIdentifier,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
};
|
||||
};
|
||||
|
||||
+26
-3
@@ -5,8 +5,10 @@ import {
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
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 { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant';
|
||||
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
|
||||
import { type UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
|
||||
import {
|
||||
@@ -19,10 +21,14 @@ import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-
|
||||
export const fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow = ({
|
||||
updateViewFieldInput: rawUpdateViewFieldInput,
|
||||
flatViewFieldMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
}: {
|
||||
updateViewFieldInput: UpdateViewFieldInput;
|
||||
flatViewFieldMaps: FlatViewFieldMaps;
|
||||
}): UniversalFlatViewField => {
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatViewFieldGroupMaps'
|
||||
>): UniversalFlatViewField => {
|
||||
const { id: viewFieldToUpdateId } =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
rawUpdateViewFieldInput,
|
||||
@@ -40,14 +46,31 @@ export const fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow = ({
|
||||
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
|
||||
rawUpdateViewFieldInput.update,
|
||||
FLAT_VIEW_FIELD_EDITABLE_PROPERTIES,
|
||||
);
|
||||
|
||||
return mergeUpdateInExistingRecord({
|
||||
const flatViewFieldToUpdate = mergeUpdateInExistingRecord({
|
||||
existing: existingFlatViewFieldToUpdate,
|
||||
properties: FLAT_VIEW_FIELD_EDITABLE_PROPERTIES,
|
||||
update: updatedEditableFieldProperties,
|
||||
});
|
||||
|
||||
if (updatedEditableFieldProperties.viewFieldGroupId !== undefined) {
|
||||
const { viewFieldGroupUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewField',
|
||||
foreignKeyValues: {
|
||||
viewFieldGroupId: flatViewFieldToUpdate.viewFieldGroupId,
|
||||
},
|
||||
flatEntityMaps: { flatViewFieldGroupMaps },
|
||||
});
|
||||
|
||||
flatViewFieldToUpdate.viewFieldGroupUniversalIdentifier =
|
||||
viewFieldGroupUniversalIdentifier;
|
||||
}
|
||||
|
||||
return flatViewFieldToUpdate;
|
||||
};
|
||||
|
||||
+18
@@ -13,6 +13,7 @@ export const fromViewFieldEntityToFlatViewField = ({
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
fieldMetadataIdToUniversalIdentifierMap,
|
||||
viewIdToUniversalIdentifierMap,
|
||||
viewFieldGroupIdToUniversalIdentifierMap,
|
||||
}: FromEntityToFlatEntityArgs<'viewField'>): FlatViewField => {
|
||||
const viewFieldEntityWithoutRelations = removePropertiesFromRecord(
|
||||
viewFieldEntity,
|
||||
@@ -52,6 +53,22 @@ export const fromViewFieldEntityToFlatViewField = ({
|
||||
);
|
||||
}
|
||||
|
||||
let viewFieldGroupUniversalIdentifier: string | null = null;
|
||||
|
||||
if (isDefined(viewFieldEntity.viewFieldGroupId)) {
|
||||
viewFieldGroupUniversalIdentifier =
|
||||
viewFieldGroupIdToUniversalIdentifierMap.get(
|
||||
viewFieldEntity.viewFieldGroupId,
|
||||
) ?? null;
|
||||
|
||||
if (!isDefined(viewFieldGroupUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`ViewFieldGroup with id ${viewFieldEntity.viewFieldGroupId} not found for viewField ${viewFieldEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...viewFieldEntityWithoutRelations,
|
||||
createdAt: viewFieldEntity.createdAt.toISOString(),
|
||||
@@ -61,5 +78,6 @@ export const fromViewFieldEntityToFlatViewField = ({
|
||||
applicationUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
viewFieldGroupUniversalIdentifier,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user