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:
+8
@@ -0,0 +1,8 @@
|
||||
import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
|
||||
|
||||
export const FLAT_VIEW_FIELD_GROUP_EDITABLE_PROPERTIES = [
|
||||
'name',
|
||||
'position',
|
||||
'isVisible',
|
||||
'deletedAt',
|
||||
] as const satisfies MetadataEntityPropertyName<'viewFieldGroup'>[];
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { fromViewFieldGroupEntityToFlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/utils/from-view-field-group-entity-to-flat-view-field-group.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';
|
||||
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
|
||||
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
|
||||
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceCache('flatViewFieldGroupMaps')
|
||||
export class WorkspaceFlatViewFieldGroupMapCacheService extends WorkspaceCacheProvider<FlatViewFieldGroupMaps> {
|
||||
constructor(
|
||||
@InjectRepository(ViewFieldGroupEntity)
|
||||
private readonly viewFieldGroupRepository: Repository<ViewFieldGroupEntity>,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
@InjectRepository(ViewEntity)
|
||||
private readonly viewRepository: Repository<ViewEntity>,
|
||||
@InjectRepository(ViewFieldEntity)
|
||||
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(workspaceId: string): Promise<FlatViewFieldGroupMaps> {
|
||||
const [viewFieldGroups, applications, views, viewFields] =
|
||||
await Promise.all([
|
||||
this.viewFieldGroupRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.applicationRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewFieldRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier', 'viewFieldGroupId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const applicationIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(applications);
|
||||
const viewIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(views);
|
||||
|
||||
const viewFieldsByViewFieldGroupId =
|
||||
regroupEntitiesByRelatedEntityId<'viewField'>({
|
||||
entities: viewFields,
|
||||
foreignKey: 'viewFieldGroupId',
|
||||
});
|
||||
|
||||
const flatViewFieldGroupMaps = createEmptyFlatEntityMaps();
|
||||
|
||||
for (const viewFieldGroupEntity of viewFieldGroups) {
|
||||
const flatViewFieldGroup = fromViewFieldGroupEntityToFlatViewFieldGroup({
|
||||
entity: {
|
||||
...viewFieldGroupEntity,
|
||||
viewFields:
|
||||
viewFieldsByViewFieldGroupId.get(viewFieldGroupEntity.id) || [],
|
||||
},
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
viewIdToUniversalIdentifierMap,
|
||||
});
|
||||
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: flatViewFieldGroup,
|
||||
flatEntityMapsToMutate: flatViewFieldGroupMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return flatViewFieldGroupMaps;
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
|
||||
|
||||
export type FlatViewFieldGroupMaps = FlatEntityMaps<FlatViewFieldGroup>;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
import { type ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
|
||||
|
||||
export type FlatViewFieldGroup = FlatEntityFrom<ViewFieldGroupEntity>;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
|
||||
import { type CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input';
|
||||
import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type';
|
||||
|
||||
export type FromCreateViewFieldGroupInputToFlatViewFieldGroupToCreateArgs = {
|
||||
createViewFieldGroupInput: CreateViewFieldGroupInput;
|
||||
flatApplication: FlatApplication;
|
||||
} & Pick<AllFlatEntityMaps, 'flatViewMaps'>;
|
||||
|
||||
export const fromCreateViewFieldGroupInputToFlatViewFieldGroupToCreate = ({
|
||||
createViewFieldGroupInput: rawCreateViewFieldGroupInput,
|
||||
flatApplication,
|
||||
flatViewMaps,
|
||||
}: FromCreateViewFieldGroupInputToFlatViewFieldGroupToCreateArgs): UniversalFlatViewFieldGroup & {
|
||||
id: string;
|
||||
} => {
|
||||
const { viewId, ...createViewFieldGroupInput } =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
rawCreateViewFieldGroupInput,
|
||||
['id', 'name', 'viewId'],
|
||||
);
|
||||
|
||||
const createdAt = new Date().toISOString();
|
||||
const viewFieldGroupId = createViewFieldGroupInput.id ?? v4();
|
||||
|
||||
const { viewUniversalIdentifier } = resolveEntityRelationUniversalIdentifiers(
|
||||
{
|
||||
metadataName: 'viewFieldGroup',
|
||||
foreignKeyValues: { viewId },
|
||||
flatEntityMaps: { flatViewMaps },
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
id: viewFieldGroupId,
|
||||
name: createViewFieldGroupInput.name,
|
||||
viewUniversalIdentifier,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
universalIdentifier: createViewFieldGroupInput.universalIdentifier ?? v4(),
|
||||
position: createViewFieldGroupInput.position ?? 0,
|
||||
isVisible: createViewFieldGroupInput.isVisible ?? true,
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
};
|
||||
};
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
extractAndSanitizeObjectStringFields,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type DeleteViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input';
|
||||
import {
|
||||
ViewFieldGroupException,
|
||||
ViewFieldGroupExceptionCode,
|
||||
} from 'src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception';
|
||||
import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type';
|
||||
|
||||
export const fromDeleteViewFieldGroupInputToFlatViewFieldGroupOrThrow = ({
|
||||
deleteViewFieldGroupInput: rawDeleteViewFieldGroupInput,
|
||||
flatViewFieldGroupMaps,
|
||||
}: {
|
||||
deleteViewFieldGroupInput: DeleteViewFieldGroupInput;
|
||||
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
|
||||
}): UniversalFlatViewFieldGroup => {
|
||||
const { id: viewFieldGroupId } = extractAndSanitizeObjectStringFields(
|
||||
rawDeleteViewFieldGroupInput,
|
||||
['id'],
|
||||
);
|
||||
|
||||
const existingFlatViewFieldGroupToDelete = findFlatEntityByIdInFlatEntityMaps(
|
||||
{
|
||||
flatEntityId: viewFieldGroupId,
|
||||
flatEntityMaps: flatViewFieldGroupMaps,
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(existingFlatViewFieldGroupToDelete)) {
|
||||
throw new ViewFieldGroupException(
|
||||
t`View field group to delete not found`,
|
||||
ViewFieldGroupExceptionCode.VIEW_FIELD_GROUP_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...existingFlatViewFieldGroupToDelete,
|
||||
deletedAt: new Date().toISOString(),
|
||||
};
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
extractAndSanitizeObjectStringFields,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type DestroyViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input';
|
||||
import {
|
||||
ViewFieldGroupException,
|
||||
ViewFieldGroupExceptionCode,
|
||||
} from 'src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception';
|
||||
import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type';
|
||||
|
||||
export const fromDestroyViewFieldGroupInputToFlatViewFieldGroupOrThrow = ({
|
||||
destroyViewFieldGroupInput,
|
||||
flatViewFieldGroupMaps,
|
||||
}: {
|
||||
destroyViewFieldGroupInput: DestroyViewFieldGroupInput;
|
||||
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
|
||||
}): UniversalFlatViewFieldGroup => {
|
||||
const { id: viewFieldGroupId } = extractAndSanitizeObjectStringFields(
|
||||
destroyViewFieldGroupInput,
|
||||
['id'],
|
||||
);
|
||||
|
||||
const existingFlatViewFieldGroupToDestroy =
|
||||
findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: viewFieldGroupId,
|
||||
flatEntityMaps: flatViewFieldGroupMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingFlatViewFieldGroupToDestroy)) {
|
||||
throw new ViewFieldGroupException(
|
||||
t`View field group to destroy not found`,
|
||||
ViewFieldGroupExceptionCode.VIEW_FIELD_GROUP_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return existingFlatViewFieldGroupToDestroy;
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
extractAndSanitizeObjectStringFields,
|
||||
isDefined,
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { FLAT_VIEW_FIELD_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field-group/constants/flat-view-field-group-editable-properties.constant';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { type UpdateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input';
|
||||
import {
|
||||
ViewFieldGroupException,
|
||||
ViewFieldGroupExceptionCode,
|
||||
} from 'src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception';
|
||||
import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type';
|
||||
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
|
||||
|
||||
export const fromUpdateViewFieldGroupInputToFlatViewFieldGroupToUpdateOrThrow =
|
||||
({
|
||||
updateViewFieldGroupInput: rawUpdateViewFieldGroupInput,
|
||||
flatViewFieldGroupMaps,
|
||||
}: {
|
||||
updateViewFieldGroupInput: UpdateViewFieldGroupInput;
|
||||
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
|
||||
}): UniversalFlatViewFieldGroup => {
|
||||
const { id: viewFieldGroupToUpdateId } =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
rawUpdateViewFieldGroupInput,
|
||||
['id'],
|
||||
);
|
||||
|
||||
const existingFlatViewFieldGroupToUpdate =
|
||||
findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: viewFieldGroupToUpdateId,
|
||||
flatEntityMaps: flatViewFieldGroupMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingFlatViewFieldGroupToUpdate)) {
|
||||
throw new ViewFieldGroupException(
|
||||
t`View field group to update not found`,
|
||||
ViewFieldGroupExceptionCode.VIEW_FIELD_GROUP_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const updatedEditableProperties = extractAndSanitizeObjectStringFields(
|
||||
rawUpdateViewFieldGroupInput.update,
|
||||
FLAT_VIEW_FIELD_GROUP_EDITABLE_PROPERTIES,
|
||||
);
|
||||
|
||||
return mergeUpdateInExistingRecord({
|
||||
existing: existingFlatViewFieldGroupToUpdate,
|
||||
properties: FLAT_VIEW_FIELD_GROUP_EDITABLE_PROPERTIES,
|
||||
update: updatedEditableProperties,
|
||||
});
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
FlatEntityMapsException,
|
||||
FlatEntityMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
|
||||
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
|
||||
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
|
||||
|
||||
export const fromViewFieldGroupEntityToFlatViewFieldGroup = ({
|
||||
entity: viewFieldGroupEntity,
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
viewIdToUniversalIdentifierMap,
|
||||
}: FromEntityToFlatEntityArgs<'viewFieldGroup'>): FlatViewFieldGroup => {
|
||||
const viewFieldGroupEntityWithoutRelations = removePropertiesFromRecord(
|
||||
viewFieldGroupEntity,
|
||||
getMetadataEntityRelationProperties('viewFieldGroup'),
|
||||
);
|
||||
|
||||
const applicationUniversalIdentifier =
|
||||
applicationIdToUniversalIdentifierMap.get(
|
||||
viewFieldGroupEntity.applicationId,
|
||||
);
|
||||
|
||||
if (!isDefined(applicationUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`Application with id ${viewFieldGroupEntity.applicationId} not found for viewFieldGroup ${viewFieldGroupEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const viewUniversalIdentifier = viewIdToUniversalIdentifierMap.get(
|
||||
viewFieldGroupEntity.viewId,
|
||||
);
|
||||
|
||||
if (!isDefined(viewUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`View with id ${viewFieldGroupEntity.viewId} not found for viewFieldGroup ${viewFieldGroupEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...viewFieldGroupEntityWithoutRelations,
|
||||
createdAt: viewFieldGroupEntity.createdAt.toISOString(),
|
||||
updatedAt: viewFieldGroupEntity.updatedAt.toISOString(),
|
||||
deletedAt: viewFieldGroupEntity.deletedAt?.toISOString() ?? null,
|
||||
universalIdentifier:
|
||||
viewFieldGroupEntityWithoutRelations.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
viewFieldIds:
|
||||
viewFieldGroupEntity.viewFields?.map((viewField) => viewField.id) ?? [],
|
||||
viewFieldUniversalIdentifiers:
|
||||
viewFieldGroupEntity.viewFields?.map(
|
||||
(viewField) => viewField.universalIdentifier,
|
||||
) ?? [],
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user