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,
@@ -12,6 +12,7 @@ import { WorkspaceFlatPageLayoutWidgetMapCacheService } from 'src/engine/metadat
import { WorkspaceFlatPageLayoutMapCacheService } from 'src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service';
import { WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-group-map-cache.service';
import { WorkspaceFlatRowLevelPermissionPredicateMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-map-cache.service';
import { WorkspaceFlatViewFieldGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-field-group/services/workspace-flat-view-field-group-map-cache.service';
import { WorkspaceFlatViewFieldMapCacheService } from 'src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service';
import { WorkspaceFlatViewFilterGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-filter-group/services/workspace-flat-view-filter-group-map-cache.service';
import { WorkspaceFlatViewFilterMapCacheService } from 'src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service';
@@ -26,6 +27,7 @@ import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entiti
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.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';
@@ -39,6 +41,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
TypeOrmModule.forFeature([
ViewEntity,
ViewFieldEntity,
ViewFieldGroupEntity,
ViewFilterEntity,
ViewFilterGroupEntity,
ViewGroupEntity,
@@ -60,6 +63,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
WorkspaceFlatObjectMetadataMapCacheService,
WorkspaceFlatViewMapCacheService,
WorkspaceFlatViewFieldMapCacheService,
WorkspaceFlatViewFieldGroupMapCacheService,
WorkspaceFlatViewFilterMapCacheService,
WorkspaceFlatViewFilterGroupMapCacheService,
WorkspaceFlatIndexMapCacheService,
@@ -76,6 +80,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
WorkspaceFlatObjectMetadataMapCacheService,
WorkspaceFlatViewMapCacheService,
WorkspaceFlatViewFieldMapCacheService,
WorkspaceFlatViewFieldGroupMapCacheService,
WorkspaceFlatViewFilterMapCacheService,
WorkspaceFlatViewFilterGroupMapCacheService,
WorkspaceFlatIndexMapCacheService,
@@ -23,6 +23,8 @@ import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-targe
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { type FlatSkillMaps } from 'src/engine/metadata-modules/flat-skill/types/flat-skill-maps.type';
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type FlatViewFilterGroupMaps } from 'src/engine/metadata-modules/flat-view-filter-group/types/flat-view-filter-group-maps.type';
@@ -57,6 +59,7 @@ import { type UniversalFlatRoleTarget } from 'src/engine/workspace-manager/works
import { type UniversalFlatRowLevelPermissionPredicate } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate.type';
import { type UniversalFlatRowLevelPermissionPredicateGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate-group.type';
import { type UniversalFlatSkill } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-skill.type';
import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type';
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
import { type UniversalFlatViewFilterGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-filter-group.type';
import { type UniversalFlatViewFilter } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-filter.type';
@@ -199,6 +202,14 @@ import {
type UniversalDeleteViewFieldAction,
type UniversalUpdateViewFieldAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/types/workspace-migration-view-field-action.type';
import {
type FlatCreateViewFieldGroupAction,
type FlatDeleteViewFieldGroupAction,
type FlatUpdateViewFieldGroupAction,
type UniversalCreateViewFieldGroupAction,
type UniversalDeleteViewFieldGroupAction,
type UniversalUpdateViewFieldGroupAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/types/workspace-migration-view-field-group-action.type';
import {
type FlatCreateViewFilterGroupAction,
type FlatDeleteViewFilterGroupAction,
@@ -308,6 +319,22 @@ export type AllFlatEntityTypesByMetadataName = {
universalFlatEntity: UniversalFlatViewField;
entity: MetadataEntity<'viewField'>;
};
viewFieldGroup: {
flatEntityMaps: FlatViewFieldGroupMaps;
universalActions: {
create: UniversalCreateViewFieldGroupAction;
update: UniversalUpdateViewFieldGroupAction;
delete: UniversalDeleteViewFieldGroupAction;
};
flatActions: {
create: FlatCreateViewFieldGroupAction;
update: FlatUpdateViewFieldGroupAction;
delete: FlatDeleteViewFieldGroupAction;
};
flatEntity: FlatViewFieldGroup;
universalFlatEntity: UniversalFlatViewFieldGroup;
entity: MetadataEntity<'viewFieldGroup'>;
};
viewGroup: {
flatEntityMaps: FlatViewGroupMaps;
universalActions: {
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`getMetadataRelatedMetadataNames should return related metadata names for agent 1`] = `[]`;
@@ -108,6 +108,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
"viewFilter",
"viewFilterGroup",
"viewGroup",
"viewFieldGroup",
]
`;
@@ -115,6 +116,14 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
[
"fieldMetadata",
"view",
"viewFieldGroup",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for viewFieldGroup 1`] = `
[
"view",
"viewField",
]
`;
@@ -4,10 +4,10 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
[
"rowLevelPermissionPredicate",
"navigationMenuItem",
"viewField",
"viewFilter",
"commandMenuItem",
"pageLayoutWidget",
"viewField",
"index",
"pageLayout",
"roleTarget",
@@ -18,9 +18,10 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
"logicFunction",
"pageLayoutTab",
"skill",
"view",
"viewFieldGroup",
"viewFilterGroup",
"webhook",
"view",
"fieldMetadata",
"role",
"objectMetadata",
@@ -80,6 +80,8 @@ export const recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate = ({
deletedAt: null,
universalIdentifier: viewFieldId,
aggregateOperation: null,
viewFieldGroupId: null,
viewFieldGroupUniversalIdentifier: null,
applicationId: existingFlatObjectMetadata.applicationId,
applicationUniversalIdentifier:
existingFlatObjectMetadata.applicationUniversalIdentifier,
@@ -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'>[];
@@ -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;
}
}
@@ -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>;
@@ -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>;
@@ -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,
};
};
@@ -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(),
};
};
@@ -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;
};
@@ -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,
});
};
@@ -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,
) ?? [],
};
};
@@ -5,4 +5,5 @@ export const FLAT_VIEW_FIELD_EDITABLE_PROPERTIES = [
'size',
'position',
'aggregateOperation',
'viewFieldGroupId',
] as const satisfies MetadataEntityPropertyName<'viewField'>[];
@@ -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({
@@ -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,
};
};
@@ -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;
};
@@ -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,
};
};
@@ -87,6 +87,7 @@ export const fromCreateViewInputToFlatViewToCreate = ({
viewFieldUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
viewGroupUniversalIdentifiers: [],
viewFieldGroupUniversalIdentifiers: [],
viewFilterGroupUniversalIdentifiers: [],
applicationUniversalIdentifier: flatApplication.universalIdentifier,
};
@@ -95,6 +95,7 @@ export const fromViewEntityToFlatView = ({
deletedAt: viewEntity.deletedAt?.toISOString() ?? null,
universalIdentifier: viewEntityWithoutRelations.universalIdentifier,
viewFieldIds: viewEntity.viewFields.map(({ id }) => id),
viewFieldGroupIds: viewEntity.viewFieldGroups?.map(({ id }) => id) ?? [],
viewFilterIds: viewEntity.viewFilters.map(({ id }) => id),
viewGroupIds: viewEntity.viewGroups.map(({ id }) => id),
viewFilterGroupIds: viewEntity.viewFilterGroups?.map(({ id }) => id) ?? [],
@@ -106,6 +107,10 @@ export const fromViewEntityToFlatView = ({
viewFieldUniversalIdentifiers: viewEntity.viewFields.map(
({ universalIdentifier }) => universalIdentifier,
),
viewFieldGroupUniversalIdentifiers:
viewEntity.viewFieldGroups?.map(
({ universalIdentifier }) => universalIdentifier,
) ?? [],
viewFilterUniversalIdentifiers: viewEntity.viewFilters.map(
({ universalIdentifier }) => universalIdentifier,
),
@@ -549,6 +549,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
viewFieldUniversalIdentifiers: [],
viewFieldGroupUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
viewGroupUniversalIdentifiers: [],
viewFilterGroupUniversalIdentifiers: [],
@@ -580,6 +581,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
.map<UniversalFlatViewField>((field, index) => ({
fieldMetadataUniversalIdentifier: field.universalIdentifier,
viewUniversalIdentifier,
viewFieldGroupUniversalIdentifier: null,
createdAt,
updatedAt: createdAt,
deletedAt: null,
@@ -0,0 +1,45 @@
import { Field, HideField, InputType } from '@nestjs/graphql';
import {
IsBoolean,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class CreateViewFieldGroupInput {
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
id?: string;
@IsNotEmpty()
@IsString()
@Field({ nullable: false })
name: string;
@IsUUID()
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
@IsOptional()
@IsNumber()
@Field({ nullable: true, defaultValue: 0 })
position?: number;
@IsOptional()
@IsBoolean()
@Field({ nullable: true, defaultValue: true })
isVisible?: boolean;
@HideField()
universalIdentifier?: string;
@HideField()
applicationId?: string;
}
@@ -0,0 +1,15 @@
import { InputType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class DeleteViewFieldGroupInput {
@IDField(() => UUIDScalarType, {
description: 'The id of the view field group to delete.',
})
@IsUUID()
id: string;
}
@@ -0,0 +1,15 @@
import { InputType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class DestroyViewFieldGroupInput {
@IDField(() => UUIDScalarType, {
description: 'The id of the view field group to destroy.',
})
@IsUUID()
id: string;
}
@@ -0,0 +1,54 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsBoolean,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
IsUUID,
ValidateNested,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
class UpdateViewFieldGroupInputUpdates {
@IsOptional()
@IsString()
@Field({ nullable: true })
name?: string;
@IsOptional()
@IsNumber()
@Field({ nullable: true })
position?: number;
@IsOptional()
@IsBoolean()
@Field({ nullable: true })
isVisible?: boolean;
@IsOptional()
@IsString()
@Field({ nullable: true })
deletedAt?: string;
}
@InputType()
export class UpdateViewFieldGroupInput {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType, {
description: 'The id of the view field group to update',
})
id: string;
@Type(() => UpdateViewFieldGroupInputUpdates)
@ValidateNested()
@Field(() => UpdateViewFieldGroupInputUpdates, {
description: 'The view field group to update',
})
update: UpdateViewFieldGroupInputUpdates;
}
@@ -0,0 +1,39 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
@ObjectType('CoreViewFieldGroup')
export class ViewFieldGroupDTO {
@IDField(() => UUIDScalarType)
id: string;
@Field({ nullable: false })
name: string;
@Field({ nullable: false, defaultValue: 0 })
position: number;
@Field({ nullable: false, defaultValue: true })
isVisible: boolean;
@Field(() => UUIDScalarType, { nullable: false })
viewId: string;
@Field(() => UUIDScalarType, { nullable: false })
workspaceId: string;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
@Field(() => Date, { nullable: true })
deletedAt?: Date | null;
@Field(() => [ViewFieldDTO])
viewFields?: ViewFieldDTO[];
}
@@ -0,0 +1,58 @@
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
type Relation,
UpdateDateColumn,
} from 'typeorm';
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 { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity({ name: 'viewFieldGroup', schema: 'core' })
@Index('IDX_VIEW_FIELD_GROUP_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
@Index('IDX_VIEW_FIELD_GROUP_VIEW_ID', ['viewId'])
export class ViewFieldGroupEntity
extends SyncableEntity
implements Required<ViewFieldGroupEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'text' })
name: string;
@Column({ nullable: false, type: 'double precision', default: 0 })
position: number;
@Column({ nullable: false, default: true })
isVisible: boolean;
@Column({ nullable: false, type: 'uuid' })
viewId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt: Date | null;
@ManyToOne(() => ViewEntity, (view) => view.viewFieldGroups, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'viewId' })
view: Relation<ViewEntity>;
@OneToMany(() => ViewFieldEntity, (viewField) => viewField.viewFieldGroup)
viewFields: Relation<ViewFieldEntity[]>;
}
@@ -0,0 +1,23 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { CustomException } from 'src/utils/custom-exception';
export class ViewFieldGroupException extends CustomException<ViewFieldGroupExceptionCode> {
constructor(
message: string,
code: ViewFieldGroupExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ?? msg`A view field group error occurred.`,
});
}
}
export enum ViewFieldGroupExceptionCode {
VIEW_FIELD_GROUP_NOT_FOUND = 'VIEW_FIELD_GROUP_NOT_FOUND',
VIEW_NOT_FOUND = 'VIEW_NOT_FOUND',
INVALID_VIEW_FIELD_GROUP_DATA = 'INVALID_VIEW_FIELD_GROUP_DATA',
}
@@ -0,0 +1,131 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import {
Args,
Context,
Mutation,
Parent,
Query,
ResolveField,
} from '@nestjs/graphql';
import { isArray } from '@sniptt/guards';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input';
import { DeleteViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input';
import { DestroyViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input';
import { UpdateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input';
import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
import { ViewFieldGroupService } from 'src/engine/metadata-modules/view-field-group/services/view-field-group.service';
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter';
@MetadataResolver(() => ViewFieldGroupDTO)
@UseFilters(ViewGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ViewFieldGroupResolver {
constructor(private readonly viewFieldGroupService: ViewFieldGroupService) {}
@Query(() => [ViewFieldGroupDTO])
@UseGuards(NoPermissionGuard)
async getCoreViewFieldGroups(
@Args('viewId', { type: () => String }) viewId: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldGroupEntity[]> {
return this.viewFieldGroupService.findByViewId(workspace.id, viewId);
}
@Query(() => ViewFieldGroupDTO, { nullable: true })
@UseGuards(NoPermissionGuard)
async getCoreViewFieldGroup(
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldGroupEntity | null> {
return this.viewFieldGroupService.findById(id, workspace.id);
}
@Mutation(() => ViewFieldGroupDTO)
@UseGuards(NoPermissionGuard)
async updateCoreViewFieldGroup(
@Args('input') updateViewFieldGroupInput: UpdateViewFieldGroupInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldGroupDTO> {
return await this.viewFieldGroupService.updateOne({
updateViewFieldGroupInput,
workspaceId,
});
}
@Mutation(() => ViewFieldGroupDTO)
@UseGuards(NoPermissionGuard)
async createCoreViewFieldGroup(
@Args('input')
createViewFieldGroupInput: CreateViewFieldGroupInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldGroupDTO> {
return await this.viewFieldGroupService.createOne({
createViewFieldGroupInput,
workspaceId,
});
}
@Mutation(() => [ViewFieldGroupDTO])
@UseGuards(NoPermissionGuard)
async createManyCoreViewFieldGroups(
@Args('inputs', { type: () => [CreateViewFieldGroupInput] })
createViewFieldGroupInputs: CreateViewFieldGroupInput[],
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldGroupDTO[]> {
return await this.viewFieldGroupService.createMany({
createViewFieldGroupInputs,
workspaceId,
});
}
@Mutation(() => ViewFieldGroupDTO)
@UseGuards(NoPermissionGuard)
async deleteCoreViewFieldGroup(
@Args('input') deleteViewFieldGroupInput: DeleteViewFieldGroupInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldGroupDTO> {
return await this.viewFieldGroupService.deleteOne({
deleteViewFieldGroupInput,
workspaceId,
});
}
@Mutation(() => ViewFieldGroupDTO)
@UseGuards(NoPermissionGuard)
async destroyCoreViewFieldGroup(
@Args('input')
destroyViewFieldGroupInput: DestroyViewFieldGroupInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldGroupDTO> {
return await this.viewFieldGroupService.destroyOne({
destroyViewFieldGroupInput,
workspaceId,
});
}
@ResolveField(() => [ViewFieldDTO])
async viewFields(
@Parent() viewFieldGroup: ViewFieldGroupDTO,
@Context() context: { loaders: IDataloaders },
@AuthWorkspace() workspace: WorkspaceEntity,
) {
if (isArray(viewFieldGroup.viewFields)) {
return viewFieldGroup.viewFields;
}
return context.loaders.viewFieldsByViewFieldGroupIdLoader.load({
workspaceId: workspace.id,
viewFieldGroupId: viewFieldGroup.id,
});
}
}
@@ -0,0 +1,366 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { fromCreateViewFieldGroupInputToFlatViewFieldGroupToCreate } from 'src/engine/metadata-modules/flat-view-field-group/utils/from-create-view-field-group-input-to-flat-view-field-group-to-create.util';
import { fromDeleteViewFieldGroupInputToFlatViewFieldGroupOrThrow } from 'src/engine/metadata-modules/flat-view-field-group/utils/from-delete-view-field-group-input-to-flat-view-field-group-or-throw.util';
import { fromDestroyViewFieldGroupInputToFlatViewFieldGroupOrThrow } from 'src/engine/metadata-modules/flat-view-field-group/utils/from-destroy-view-field-group-input-to-flat-view-field-group-or-throw.util';
import { fromUpdateViewFieldGroupInputToFlatViewFieldGroupToUpdateOrThrow } from 'src/engine/metadata-modules/flat-view-field-group/utils/from-update-view-field-group-input-to-flat-view-field-group-to-update-or-throw.util';
import { CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input';
import { DeleteViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input';
import { DestroyViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input';
import { UpdateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input';
import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
import {
ViewFieldGroupException,
ViewFieldGroupExceptionCode,
} from 'src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception';
import { fromFlatViewFieldGroupToViewFieldGroupDto } from 'src/engine/metadata-modules/view-field-group/utils/from-flat-view-field-group-to-view-field-group-dto.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class ViewFieldGroupService {
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
@InjectRepository(ViewFieldGroupEntity)
private readonly viewFieldGroupRepository: Repository<ViewFieldGroupEntity>,
private readonly applicationService: ApplicationService,
) {}
async createOne({
createViewFieldGroupInput,
workspaceId,
}: {
createViewFieldGroupInput: CreateViewFieldGroupInput;
workspaceId: string;
}): Promise<ViewFieldGroupDTO> {
const [createdViewFieldGroup] = await this.createMany({
workspaceId,
createViewFieldGroupInputs: [createViewFieldGroupInput],
});
if (!isDefined(createdViewFieldGroup)) {
throw new ViewFieldGroupException(
'Failed to create view field group',
ViewFieldGroupExceptionCode.INVALID_VIEW_FIELD_GROUP_DATA,
);
}
return createdViewFieldGroup;
}
async createMany({
createViewFieldGroupInputs,
workspaceId,
}: {
createViewFieldGroupInputs: CreateViewFieldGroupInput[];
workspaceId: string;
}): Promise<ViewFieldGroupDTO[]> {
if (createViewFieldGroupInputs.length === 0) {
return [];
}
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const { flatViewMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps'],
},
);
const flatViewFieldGroupsToCreate = createViewFieldGroupInputs.map(
(createViewFieldGroupInput) =>
fromCreateViewFieldGroupInputToFlatViewFieldGroupToCreate({
createViewFieldGroupInput,
flatApplication: workspaceCustomFlatApplication,
flatViewMaps,
}),
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
viewFieldGroup: {
flatEntityToCreate: flatViewFieldGroupsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating view field groups',
);
}
const { flatViewFieldGroupMaps: recomputedExistingFlatViewFieldGroupMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldGroupMaps'],
},
);
return findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: flatViewFieldGroupsToCreate.map((entity) => entity.id),
flatEntityMaps: recomputedExistingFlatViewFieldGroupMaps,
}).map(fromFlatViewFieldGroupToViewFieldGroupDto);
}
async updateOne({
updateViewFieldGroupInput,
workspaceId,
}: {
workspaceId: string;
updateViewFieldGroupInput: UpdateViewFieldGroupInput;
}): Promise<ViewFieldGroupDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const { flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldGroupMaps'],
},
);
const optimisticallyUpdatedFlatViewFieldGroup =
fromUpdateViewFieldGroupInputToFlatViewFieldGroupToUpdateOrThrow({
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
updateViewFieldGroupInput,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
viewFieldGroup: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [optimisticallyUpdatedFlatViewFieldGroup],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating view field group',
);
}
const { flatViewFieldGroupMaps: recomputedExistingFlatViewFieldGroupMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldGroupMaps'],
},
);
return fromFlatViewFieldGroupToViewFieldGroupDto(
findFlatEntityByUniversalIdentifierOrThrow({
universalIdentifier:
optimisticallyUpdatedFlatViewFieldGroup.universalIdentifier,
flatEntityMaps: recomputedExistingFlatViewFieldGroupMaps,
}),
);
}
async deleteOne({
deleteViewFieldGroupInput,
workspaceId,
}: {
deleteViewFieldGroupInput: DeleteViewFieldGroupInput;
workspaceId: string;
}): Promise<ViewFieldGroupDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const { flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldGroupMaps'],
},
);
const optimisticallyUpdatedFlatViewFieldGroupWithDeletedAt =
fromDeleteViewFieldGroupInputToFlatViewFieldGroupOrThrow({
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
deleteViewFieldGroupInput,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
viewFieldGroup: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [
optimisticallyUpdatedFlatViewFieldGroupWithDeletedAt,
],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting view field group',
);
}
const { flatViewFieldGroupMaps: recomputedExistingFlatViewFieldGroupMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldGroupMaps'],
},
);
return fromFlatViewFieldGroupToViewFieldGroupDto(
findFlatEntityByUniversalIdentifierOrThrow({
universalIdentifier:
optimisticallyUpdatedFlatViewFieldGroupWithDeletedAt.universalIdentifier,
flatEntityMaps: recomputedExistingFlatViewFieldGroupMaps,
}),
);
}
async destroyOne({
destroyViewFieldGroupInput,
workspaceId,
}: {
destroyViewFieldGroupInput: DestroyViewFieldGroupInput;
workspaceId: string;
}): Promise<ViewFieldGroupDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const { flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldGroupMaps'],
},
);
const existingViewFieldGroupToDelete =
fromDestroyViewFieldGroupInputToFlatViewFieldGroupOrThrow({
destroyViewFieldGroupInput,
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
});
const existingFlatViewFieldGroup =
findFlatEntityByUniversalIdentifierOrThrow({
universalIdentifier: existingViewFieldGroupToDelete.universalIdentifier,
flatEntityMaps: existingFlatViewFieldGroupMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
viewFieldGroup: {
flatEntityToCreate: [],
flatEntityToDelete: [existingViewFieldGroupToDelete],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying view field group',
);
}
return fromFlatViewFieldGroupToViewFieldGroupDto({
...existingFlatViewFieldGroup,
deletedAt: new Date().toISOString(),
});
}
async findByViewId(
workspaceId: string,
viewId: string,
): Promise<ViewFieldGroupEntity[]> {
return this.viewFieldGroupRepository.find({
where: {
workspaceId,
viewId,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
});
}
async findById(
id: string,
workspaceId: string,
): Promise<ViewFieldGroupEntity | null> {
const viewFieldGroup = await this.viewFieldGroupRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
});
return viewFieldGroup || null;
}
}
@@ -0,0 +1,15 @@
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
import { type ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
export const fromFlatViewFieldGroupToViewFieldGroupDto = (
flatViewFieldGroup: FlatViewFieldGroup,
): ViewFieldGroupDTO => {
const { createdAt, updatedAt, deletedAt, ...rest } = flatViewFieldGroup;
return {
...rest,
createdAt: new Date(createdAt),
updatedAt: new Date(updatedAt),
deletedAt: deletedAt ? new Date(deletedAt) : null,
};
};
@@ -0,0 +1,26 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
import { ViewFieldGroupResolver } from 'src/engine/metadata-modules/view-field-group/resolvers/view-field-group.resolver';
import { ViewFieldGroupService } from 'src/engine/metadata-modules/view-field-group/services/view-field-group.service';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
TypeOrmModule.forFeature([ViewFieldGroupEntity, ViewEntity]),
WorkspaceCacheStorageModule,
ApplicationModule,
PermissionsModule,
WorkspaceMigrationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [ViewFieldGroupResolver, ViewFieldGroupService],
exports: [ViewFieldGroupService],
})
export class ViewFieldGroupModule {}
@@ -29,7 +29,7 @@ import {
ViewFieldExceptionMessageKey,
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
import { ViewFieldRestApiExceptionFilter } from 'src/engine/metadata-modules/view-field/filters/view-field-rest-api-exception.filter';
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 { CreateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-field-permission.guard';
import { DeleteViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-field-permission.guard';
import { UpdateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-field-permission.guard';
@@ -38,7 +38,7 @@ import { UpdateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewFieldRestApiExceptionFilter)
export class ViewFieldController {
constructor(private readonly viewFieldV2Service: ViewFieldV2Service) {}
constructor(private readonly viewFieldService: ViewFieldService) {}
@Get()
@UseGuards(NoPermissionGuard)
@@ -47,10 +47,10 @@ export class ViewFieldController {
@Query('viewId') viewId?: string,
): Promise<ViewFieldEntity[]> {
if (viewId) {
return this.viewFieldV2Service.findByViewId(workspace.id, viewId);
return this.viewFieldService.findByViewId(workspace.id, viewId);
}
return this.viewFieldV2Service.findByWorkspaceId(workspace.id);
return this.viewFieldService.findByWorkspaceId(workspace.id);
}
@Get(':id')
@@ -59,7 +59,7 @@ export class ViewFieldController {
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldEntity> {
const viewField = await this.viewFieldV2Service.findById(id, workspace.id);
const viewField = await this.viewFieldService.findById(id, workspace.id);
if (!isDefined(viewField)) {
throw new ViewFieldException(
@@ -86,7 +86,7 @@ export class ViewFieldController {
@Body() input: UpdateViewFieldInput['update'],
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldDTO> {
return await this.viewFieldV2Service.updateOne({
return await this.viewFieldService.updateOne({
updateViewFieldInput: { id, update: input },
workspaceId: workspace.id,
});
@@ -98,7 +98,7 @@ export class ViewFieldController {
@Body() input: CreateViewFieldInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldDTO> {
return await this.viewFieldV2Service.createOne({
return await this.viewFieldService.createOne({
createViewFieldInput: input,
workspaceId: workspace.id,
});
@@ -110,7 +110,7 @@ export class ViewFieldController {
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<{ success: boolean }> {
const deletedViewField = await this.viewFieldV2Service.deleteOne({
const deletedViewField = await this.viewFieldService.deleteOne({
deleteViewFieldInput: { id },
workspaceId: workspace.id,
});
@@ -46,6 +46,11 @@ export class CreateViewFieldInput {
@Field(() => AggregateOperations, { nullable: true })
aggregateOperation?: AggregateOperations;
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
viewFieldGroupId?: string;
@HideField()
universalIdentifier?: string;
@@ -35,6 +35,11 @@ class UpdateViewFieldInputUpdates {
@IsEnum(AggregateOperations)
@Field(() => AggregateOperations, { nullable: true })
aggregateOperation?: AggregateOperations;
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
viewFieldGroupId?: string | null;
}
@InputType()
@@ -13,6 +13,7 @@ import {
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@@ -64,6 +65,9 @@ export class ViewFieldEntity
@Column({ nullable: false, type: 'uuid' })
viewId: string;
@Column({ nullable: true, type: 'uuid' })
viewFieldGroupId: string | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -78,4 +82,15 @@ export class ViewFieldEntity
})
@JoinColumn({ name: 'viewId' })
view: Relation<ViewEntity>;
@ManyToOne(
() => ViewFieldGroupEntity,
(viewFieldGroup) => viewFieldGroup.viewFields,
{
onDelete: 'SET NULL',
nullable: true,
},
)
@JoinColumn({ name: 'viewFieldGroupId' })
viewFieldGroup: Relation<ViewFieldGroupEntity> | null;
}
@@ -1,9 +1,9 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query } from '@nestjs/graphql';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input';
@@ -12,7 +12,7 @@ import { DestroyViewFieldInput } from 'src/engine/metadata-modules/view-field/dt
import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
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 { CreateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-field-permission.guard';
import { DeleteViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-field-permission.guard';
import { DestroyViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/destroy-view-field-permission.guard';
@@ -23,7 +23,7 @@ import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/
@UseFilters(ViewGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ViewFieldResolver {
constructor(private readonly viewFieldV2Service: ViewFieldV2Service) {}
constructor(private readonly viewFieldService: ViewFieldService) {}
@Query(() => [ViewFieldDTO])
@UseGuards(NoPermissionGuard)
@@ -31,7 +31,7 @@ export class ViewFieldResolver {
@Args('viewId', { type: () => String }) viewId: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldEntity[]> {
return this.viewFieldV2Service.findByViewId(workspace.id, viewId);
return this.viewFieldService.findByViewId(workspace.id, viewId);
}
@Query(() => ViewFieldDTO, { nullable: true })
@@ -40,7 +40,7 @@ export class ViewFieldResolver {
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldEntity | null> {
return this.viewFieldV2Service.findById(id, workspace.id);
return this.viewFieldService.findById(id, workspace.id);
}
@Mutation(() => ViewFieldDTO)
@@ -49,7 +49,7 @@ export class ViewFieldResolver {
@Args('input') updateViewFieldInput: UpdateViewFieldInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldDTO> {
return await this.viewFieldV2Service.updateOne({
return await this.viewFieldService.updateOne({
updateViewFieldInput,
workspaceId,
});
@@ -61,7 +61,7 @@ export class ViewFieldResolver {
@Args('input') createViewFieldInput: CreateViewFieldInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldDTO> {
return await this.viewFieldV2Service.createOne({
return await this.viewFieldService.createOne({
createViewFieldInput,
workspaceId,
});
@@ -74,7 +74,7 @@ export class ViewFieldResolver {
createViewFieldInputs: CreateViewFieldInput[],
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldDTO[]> {
return await this.viewFieldV2Service.createMany({
return await this.viewFieldService.createMany({
createViewFieldInputs,
workspaceId,
});
@@ -86,7 +86,7 @@ export class ViewFieldResolver {
@Args('input') deleteViewFieldInput: DeleteViewFieldInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldDTO> {
return await this.viewFieldV2Service.deleteOne({
return await this.viewFieldService.deleteOne({
deleteViewFieldInput,
workspaceId,
});
@@ -98,7 +98,7 @@ export class ViewFieldResolver {
@Args('input') destroyViewFieldInput: DestroyViewFieldInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldDTO> {
return await this.viewFieldV2Service.destroyOne({
return await this.viewFieldService.destroyOne({
destroyViewFieldInput,
workspaceId,
});
@@ -27,7 +27,7 @@ import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class ViewFieldV2Service {
export class ViewFieldService {
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
@@ -76,11 +76,15 @@ export class ViewFieldV2Service {
},
);
const { flatFieldMetadataMaps, flatViewMaps } =
const { flatFieldMetadataMaps, flatViewMaps, flatViewFieldGroupMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatFieldMetadataMaps', 'flatViewMaps'],
flatMapsKeys: [
'flatFieldMetadataMaps',
'flatViewMaps',
'flatViewFieldGroupMaps',
],
},
);
@@ -91,6 +95,7 @@ export class ViewFieldV2Service {
flatApplication: workspaceCustomFlatApplication,
flatFieldMetadataMaps,
flatViewMaps,
flatViewFieldGroupMaps,
}),
);
@@ -146,17 +151,21 @@ export class ViewFieldV2Service {
},
);
const { flatViewFieldMaps: existingFlatViewFieldMaps } =
const {
flatViewFieldMaps: existingFlatViewFieldMaps,
flatViewFieldGroupMaps,
} =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldMaps'],
flatMapsKeys: ['flatViewFieldMaps', 'flatViewFieldGroupMaps'],
},
);
const optimisticallyUpdatedFlatView =
fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow({
flatViewFieldMaps: existingFlatViewFieldMaps,
flatViewFieldGroupMaps,
updateViewFieldInput,
});
@@ -7,7 +7,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
import { ViewFieldController } from 'src/engine/metadata-modules/view-field/controllers/view-field.controller';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFieldResolver } from 'src/engine/metadata-modules/view-field/resolvers/view-field.resolver';
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 { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.module';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@@ -24,7 +24,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
ViewPermissionsModule,
],
controllers: [ViewFieldController],
providers: [ViewFieldResolver, ViewFieldV2Service],
exports: [ViewFieldV2Service],
providers: [ViewFieldResolver, ViewFieldService],
exports: [ViewFieldService],
})
export class ViewFieldModule {}
@@ -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,
});
}
}
@@ -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:
@@ -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,