breaking (soft) - Migrate viewGroup.fieldMetadataId -> view.mainGroupByFieldMetadataId (2/3) (#16277)
Should be merged once https://github.com/twentyhq/twenty/pull/16206 has been released + command run to prod In this PR - Remove usage of viewGroup.fieldMetadataId, both in BE and FE states. - But we still need to properly populate it until we fully remove viewGroup.fieldMetadataId from db and ORM entity (upcoming 3rd PR out of 3). fieldMetadataId was removed from CoreViewGroup type and CreateViewGroupInput and is determined BE-side based on the associated view's mainGroupByFieldMetadataId. **I expect this means a downtime on viewGroup creation, until both FE and BE are deployed and cache is flushed.** This seems acceptable to me as it only regards viewGroup creation. - this information is replaced by view.mainGroupByFieldMetadataID - Handle view group creation, update and deletion in the BE as a side-effect of a view creation, update or deletion. Optimistic effects are still used - Add validation at view creation or update regarding mainGroupByFieldMetadata Left to do in 3rd PR - Remove viewGroup.fieldMetadataId from db and ORM entity - Restore feature allowing to update an existing grouped view's group by field (already OK on BE side but need to rebuild FE optimistic)
This commit is contained in:
-4
@@ -65,10 +65,6 @@ export const ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY = {
|
||||
metadataName: 'view',
|
||||
flatEntityForeignKeyAggregator: 'viewGroupIds',
|
||||
},
|
||||
fieldMetadataId: {
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntityForeignKeyAggregator: 'viewGroupIds',
|
||||
},
|
||||
},
|
||||
index: {
|
||||
objectMetadataId: {
|
||||
|
||||
+5
-5
@@ -27,7 +27,7 @@ describe('addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow',
|
||||
applicationId,
|
||||
});
|
||||
|
||||
const mockFieldMEtadata = getFlatFieldMetadataMock({
|
||||
const mockFieldMetadata = getFlatFieldMetadataMock({
|
||||
objectMetadataId,
|
||||
id: '202020-71a3-4856-a3d0-d08cea0ecec6',
|
||||
type: FieldMetadataType.DATE,
|
||||
@@ -35,9 +35,9 @@ describe('addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow',
|
||||
applicationId,
|
||||
universalIdentifier: 'field-universal-1',
|
||||
viewFieldIds: [],
|
||||
viewGroupIds: [],
|
||||
viewFilterIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
});
|
||||
|
||||
const mockView: Pick<FlatView, 'id'> & Partial<FlatView> = {
|
||||
@@ -49,13 +49,13 @@ describe('addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow',
|
||||
viewFilterIds: [],
|
||||
viewGroupIds: [],
|
||||
applicationId,
|
||||
calendarFieldMetadataId: mockFieldMEtadata.id,
|
||||
calendarFieldMetadataId: mockFieldMetadata.id,
|
||||
};
|
||||
|
||||
const flatEntityAndRelatedMapsToMutate: MetadataFlatEntityAndRelatedFlatEntityMaps<'view'> =
|
||||
{
|
||||
flatFieldMetadataMaps: addFlatEntityToFlatEntityMapsOrThrow({
|
||||
flatEntity: mockFieldMEtadata,
|
||||
flatEntity: mockFieldMetadata,
|
||||
flatEntityMaps: createEmptyFlatEntityMaps(),
|
||||
}),
|
||||
flatObjectMetadataMaps: addFlatEntityToFlatEntityMapsOrThrow({
|
||||
@@ -85,7 +85,7 @@ describe('addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow',
|
||||
|
||||
expect(
|
||||
flatEntityAndRelatedMapsToMutate.flatFieldMetadataMaps.byId[
|
||||
mockFieldMEtadata.id
|
||||
mockFieldMetadata.id
|
||||
],
|
||||
).toMatchObject<Partial<FlatFieldMetadata>>({
|
||||
calendarViewIds: [mockView.id],
|
||||
|
||||
+1
-1
@@ -34,10 +34,10 @@ describe('deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThr
|
||||
type: FieldMetadataType.DATE,
|
||||
universalIdentifier: 'field-universal-1',
|
||||
viewFieldIds: [],
|
||||
viewGroupIds: [],
|
||||
viewFilterIds: [],
|
||||
workspaceId,
|
||||
calendarViewIds: [viewId],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
applicationId,
|
||||
});
|
||||
|
||||
|
||||
-1
@@ -21,7 +21,6 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
|
||||
return {
|
||||
calendarViewIds: [],
|
||||
viewFilterIds: [],
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
viewFieldIds: [],
|
||||
createdAt,
|
||||
|
||||
-1
@@ -35,7 +35,6 @@ export const getRelationTargetFlatFieldMetadataMock = ({
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
viewFilterIds: [],
|
||||
viewGroupIds: [],
|
||||
viewFieldIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
createdAt,
|
||||
|
||||
-4
@@ -137,7 +137,6 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"viewFieldIds": [],
|
||||
"viewFilterIds": [],
|
||||
"viewGroupIds": [],
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
{
|
||||
@@ -176,7 +175,6 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"viewFieldIds": [],
|
||||
"viewFilterIds": [],
|
||||
"viewGroupIds": [],
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
{
|
||||
@@ -213,7 +211,6 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"viewFieldIds": [],
|
||||
"viewFilterIds": [],
|
||||
"viewGroupIds": [],
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
{
|
||||
@@ -252,7 +249,6 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"updatedAt": Any<ClockDate>,
|
||||
"viewFieldIds": [],
|
||||
"viewFilterIds": [],
|
||||
"viewGroupIds": [],
|
||||
"workspaceId": Any<String>,
|
||||
},
|
||||
],
|
||||
|
||||
+26
-38
@@ -41,43 +41,36 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<FlatEntityMaps<FlatFieldMetadata>> {
|
||||
const [fieldMetadatas, viewFields, viewFilters, viewGroups, views] =
|
||||
await Promise.all([
|
||||
this.fieldMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewFieldRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'fieldMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewFilterRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'fieldMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewGroupRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'fieldMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewRepository.find({
|
||||
where: { workspaceId },
|
||||
select: [
|
||||
'id',
|
||||
'kanbanAggregateOperationFieldMetadataId',
|
||||
'calendarFieldMetadataId',
|
||||
'mainGroupByFieldMetadataId',
|
||||
],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
const [fieldMetadatas, viewFields, viewFilters, views] = await Promise.all([
|
||||
this.fieldMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewFieldRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'fieldMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewFilterRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'fieldMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewRepository.find({
|
||||
where: { workspaceId },
|
||||
select: [
|
||||
'id',
|
||||
'kanbanAggregateOperationFieldMetadataId',
|
||||
'calendarFieldMetadataId',
|
||||
'mainGroupByFieldMetadataId',
|
||||
],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const [
|
||||
viewFieldsByFieldId,
|
||||
viewFiltersByFieldId,
|
||||
viewGroupsByFieldId,
|
||||
calendarViewsByFieldId,
|
||||
kanbanViewsByFieldId,
|
||||
mainGroupByFieldMetadataViewsByFieldId,
|
||||
@@ -91,10 +84,6 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
|
||||
entities: viewFilters,
|
||||
foreignKey: 'fieldMetadataId',
|
||||
},
|
||||
{
|
||||
entities: viewGroups,
|
||||
foreignKey: 'fieldMetadataId',
|
||||
},
|
||||
{
|
||||
entities: views,
|
||||
foreignKey: 'calendarFieldMetadataId',
|
||||
@@ -117,7 +106,6 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
|
||||
...fieldMetadataEntity,
|
||||
viewFields: viewFieldsByFieldId.get(fieldMetadataEntity.id) || [],
|
||||
viewFilters: viewFiltersByFieldId.get(fieldMetadataEntity.id) || [],
|
||||
viewGroups: viewGroupsByFieldId.get(fieldMetadataEntity.id) || [],
|
||||
kanbanAggregateOperationViews:
|
||||
kanbanViewsByFieldId.get(fieldMetadataEntity.id) || [],
|
||||
calendarViews: calendarViewsByFieldId.get(fieldMetadataEntity.id) || [],
|
||||
|
||||
+4
-3
@@ -14,19 +14,20 @@ export const FIELD_METADATA_RELATION_PROPERTIES = [
|
||||
'kanbanAggregateOperationViews',
|
||||
'calendarViews',
|
||||
'mainGroupByFieldMetadataViews',
|
||||
'viewGroups',
|
||||
] as const satisfies (keyof FieldMetadataEntity)[];
|
||||
|
||||
export type FieldMetadataEntityRelationProperties =
|
||||
(typeof FIELD_METADATA_RELATION_PROPERTIES)[number];
|
||||
|
||||
export type FlatFieldMetadata<T extends FieldMetadataType = FieldMetadataType> =
|
||||
Omit<FieldMetadataEntity<T>, FieldMetadataEntityRelationProperties> & {
|
||||
Omit<
|
||||
FieldMetadataEntity<T>,
|
||||
FieldMetadataEntityRelationProperties | 'viewGroups'
|
||||
> & {
|
||||
universalIdentifier: string;
|
||||
viewFieldIds: string[];
|
||||
viewFilterIds: string[];
|
||||
kanbanAggregateOperationViewIds: string[];
|
||||
calendarViewIds: string[];
|
||||
mainGroupByFieldMetadataViewIds: string[];
|
||||
viewGroupIds: string[];
|
||||
};
|
||||
|
||||
-1
@@ -26,7 +26,6 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
|
||||
mainGroupByFieldMetadataViewIds:
|
||||
fieldMetadataEntity.mainGroupByFieldMetadataViews?.map(({ id }) => id) ??
|
||||
[],
|
||||
viewGroupIds: fieldMetadataEntity.viewGroups.map(({ id }) => id),
|
||||
viewFieldIds: fieldMetadataEntity.viewFields.map(({ id }) => id),
|
||||
viewFilterIds: fieldMetadataEntity.viewFilters.map(({ id }) => id),
|
||||
universalIdentifier:
|
||||
|
||||
-1
@@ -60,7 +60,6 @@ export const getDefaultFlatFieldMetadata = ({
|
||||
morphId: null,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
viewFilterIds: [],
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
} as const satisfies FlatFieldMetadata;
|
||||
};
|
||||
|
||||
+7
-1
@@ -17,7 +17,10 @@ type HandleEnumFlatFieldMetadataOptionsUpdateSideEffectsArgs = FromTo<
|
||||
FlatFieldMetadata<EnumFieldMetadataType>,
|
||||
'flatFieldMetadata'
|
||||
> &
|
||||
Pick<AllFlatEntityMaps, 'flatViewFilterMaps' | 'flatViewGroupMaps'>;
|
||||
Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatViewFilterMaps' | 'flatViewGroupMaps' | 'flatViewMaps'
|
||||
>;
|
||||
|
||||
type EnumFieldMetadataSideEffectResult = FlatViewGroupsToDeleteUpdateAndCreate &
|
||||
FlatViewFiltersToDeleteAndUpdate;
|
||||
@@ -36,6 +39,7 @@ export const handleEnumFlatFieldMetadataUpdateSideEffects = ({
|
||||
toFlatFieldMetadata,
|
||||
flatViewFilterMaps,
|
||||
flatViewGroupMaps,
|
||||
flatViewMaps,
|
||||
}: HandleEnumFlatFieldMetadataOptionsUpdateSideEffectsArgs): EnumFieldMetadataSideEffectResult => {
|
||||
const sideEffectResult = structuredClone(
|
||||
EMPTY_ENUM_FIELD_METADATA_SIDE_EFFECT_RESULT,
|
||||
@@ -69,6 +73,7 @@ export const handleEnumFlatFieldMetadataUpdateSideEffects = ({
|
||||
flatViewGroupsToDelete,
|
||||
flatViewGroupsToUpdate,
|
||||
} = recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate({
|
||||
flatViewMaps,
|
||||
flatViewGroupMaps,
|
||||
fromFlatFieldMetadata,
|
||||
update: optionsPropertyUpdate,
|
||||
@@ -82,6 +87,7 @@ export const handleEnumFlatFieldMetadataUpdateSideEffects = ({
|
||||
if (fromFlatFieldMetadata.isNullable !== toFlatFieldMetadata.isNullable) {
|
||||
const { flatViewGroupsToCreate, flatViewGroupsToDelete } =
|
||||
recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate({
|
||||
flatViewMaps,
|
||||
flatViewGroupMaps,
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
|
||||
+10
-1
@@ -46,8 +46,16 @@ export const handleFieldMetadataDeactivationSideEffects = ({
|
||||
flatEntityMaps: flatViewFieldMaps,
|
||||
});
|
||||
|
||||
const flatViewsAffected: FlatView[] =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: fromFlatFieldMetadata.mainGroupByFieldMetadataViewIds,
|
||||
flatEntityMaps: flatViewMaps,
|
||||
});
|
||||
|
||||
const flatViewGroups = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: fromFlatFieldMetadata.viewGroupIds,
|
||||
flatEntityIds: flatViewsAffected.flatMap(
|
||||
(flatView) => flatView.viewGroupIds,
|
||||
),
|
||||
flatEntityMaps: flatViewGroupMaps,
|
||||
});
|
||||
|
||||
@@ -60,6 +68,7 @@ export const handleFieldMetadataDeactivationSideEffects = ({
|
||||
...new Set([
|
||||
...Object.keys(flatViewGroupRecordByViewId),
|
||||
...fromFlatFieldMetadata.calendarViewIds,
|
||||
...fromFlatFieldMetadata.mainGroupByFieldMetadataViewIds,
|
||||
]),
|
||||
];
|
||||
|
||||
|
||||
+1
@@ -100,6 +100,7 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
|
||||
flatViewGroupsToDelete,
|
||||
flatViewGroupsToUpdate,
|
||||
} = handleEnumFlatFieldMetadataUpdateSideEffects({
|
||||
flatViewMaps,
|
||||
flatViewFilterMaps,
|
||||
flatViewGroupMaps,
|
||||
fromFlatFieldMetadata,
|
||||
|
||||
+11
-3
@@ -12,7 +12,7 @@ type RecomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdateArgs = FromTo<
|
||||
FlatFieldMetadata,
|
||||
'flatFieldMetadata'
|
||||
> &
|
||||
Pick<AllFlatEntityMaps, 'flatViewGroupMaps'>;
|
||||
Pick<AllFlatEntityMaps, 'flatViewMaps' | 'flatViewGroupMaps'>;
|
||||
|
||||
type EnumFieldMetadataIsNullableUpdateSideEffect = {
|
||||
flatViewGroupsToDelete: FlatViewGroup[];
|
||||
@@ -25,6 +25,7 @@ const EMPTY_ENUM_FIELD_METADATA_IS_NULLABLE_UPDATE_SIDE_EFFECT_RESULT: EnumField
|
||||
};
|
||||
|
||||
export const recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate = ({
|
||||
flatViewMaps,
|
||||
flatViewGroupMaps: allFlatViewGroups,
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
@@ -35,8 +36,15 @@ export const recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate = ({
|
||||
const sideEffectResult = structuredClone(
|
||||
EMPTY_ENUM_FIELD_METADATA_IS_NULLABLE_UPDATE_SIDE_EFFECT_RESULT,
|
||||
);
|
||||
const flatViewsAffected = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: fromFlatFieldMetadata.mainGroupByFieldMetadataViewIds,
|
||||
flatEntityMaps: flatViewMaps,
|
||||
});
|
||||
|
||||
const flatViewGroups = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: fromFlatFieldMetadata.viewGroupIds,
|
||||
flatEntityIds: flatViewsAffected.flatMap(
|
||||
(flatView) => flatView.viewGroupIds,
|
||||
),
|
||||
flatEntityMaps: allFlatViewGroups,
|
||||
});
|
||||
const { flatViewGroupRecordByViewId, highestViewGroupPositionByViewId } =
|
||||
@@ -60,7 +68,6 @@ export const recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate = ({
|
||||
const createdAt = new Date();
|
||||
|
||||
sideEffectResult.flatViewGroupsToCreate.push({
|
||||
fieldMetadataId: toFlatFieldMetadata.id,
|
||||
id: viewGroupId,
|
||||
universalIdentifier: viewGroupId,
|
||||
fieldValue: '',
|
||||
@@ -72,6 +79,7 @@ export const recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate = ({
|
||||
deletedAt: null,
|
||||
viewId,
|
||||
applicationId: toFlatFieldMetadata.applicationId,
|
||||
fieldMetadataId: fromFlatFieldMetadata.id,
|
||||
});
|
||||
} else if (isDefined(emptyValueFlatViewGroup)) {
|
||||
sideEffectResult.flatViewGroupsToDelete.push(emptyValueFlatViewGroup);
|
||||
|
||||
+11
-4
@@ -17,7 +17,7 @@ import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migr
|
||||
type RecomputeViewGroupsOnFlatFieldMetadataOptionsUpdateArgs = {
|
||||
fromFlatFieldMetadata: FlatFieldMetadata<EnumFieldMetadataType>;
|
||||
update: PropertyUpdate<FlatFieldMetadata<EnumFieldMetadataType>, 'options'>;
|
||||
} & Pick<AllFlatEntityMaps, 'flatViewGroupMaps'>;
|
||||
} & Pick<AllFlatEntityMaps, 'flatViewMaps' | 'flatViewGroupMaps'>;
|
||||
|
||||
export type FlatViewGroupsToDeleteUpdateAndCreate = {
|
||||
flatViewGroupsToDelete: FlatViewGroup[];
|
||||
@@ -25,6 +25,7 @@ export type FlatViewGroupsToDeleteUpdateAndCreate = {
|
||||
flatViewGroupsToCreate: FlatViewGroup[];
|
||||
};
|
||||
export const recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate = ({
|
||||
flatViewMaps,
|
||||
flatViewGroupMaps,
|
||||
fromFlatFieldMetadata,
|
||||
update,
|
||||
@@ -39,8 +40,15 @@ export const recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate = ({
|
||||
toOptions: update.to,
|
||||
});
|
||||
|
||||
const flatViewsAffected = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: fromFlatFieldMetadata.mainGroupByFieldMetadataViewIds,
|
||||
flatEntityMaps: flatViewMaps,
|
||||
});
|
||||
|
||||
const flatViewGroups = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: fromFlatFieldMetadata.viewGroupIds,
|
||||
flatEntityIds: flatViewsAffected.flatMap(
|
||||
(flatView) => flatView.viewGroupIds,
|
||||
),
|
||||
flatEntityMaps: flatViewGroupMaps,
|
||||
});
|
||||
|
||||
@@ -63,8 +71,7 @@ export const recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate = ({
|
||||
const flatViewGroupsToUpdate = updatedFieldMetadataOptions.flatMap(
|
||||
({ from: fromOption, to: toOption }) =>
|
||||
flatViewGroups.flatMap((flatViewGroup) =>
|
||||
flatViewGroup.fieldValue === fromOption.value &&
|
||||
flatViewGroup.fieldMetadataId === fromFlatFieldMetadata.id
|
||||
flatViewGroup.fieldValue === fromOption.value
|
||||
? { ...flatViewGroup, fieldValue: toOption.value }
|
||||
: [],
|
||||
),
|
||||
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
import { type ViewGroupEntityRelationProperties } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
|
||||
|
||||
export const VIEW_GROUP_ENTITY_RELATION_PROPERTIES = [
|
||||
'fieldMetadata',
|
||||
'view',
|
||||
'workspace',
|
||||
] as const satisfies ViewGroupEntityRelationProperties[];
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
FlatEntityMapsException,
|
||||
FlatEntityMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
|
||||
|
||||
type ComputeFlatViewGroupsOnViewCreateArgs = {
|
||||
flatViewToCreateId: string;
|
||||
mainGroupByFieldMetadataId: string;
|
||||
} & Pick<AllFlatEntityMaps, 'flatFieldMetadataMaps'>;
|
||||
|
||||
export const computeFlatViewGroupsOnViewCreate = ({
|
||||
flatViewToCreateId,
|
||||
mainGroupByFieldMetadataId,
|
||||
flatFieldMetadataMaps,
|
||||
}: ComputeFlatViewGroupsOnViewCreateArgs): FlatViewGroup[] => {
|
||||
const mainGroupByFieldMetadata =
|
||||
flatFieldMetadataMaps.byId[mainGroupByFieldMetadataId];
|
||||
|
||||
if (!isDefined(mainGroupByFieldMetadata)) {
|
||||
throw new FlatEntityMapsException(
|
||||
'mainGroupByFieldMetadataId not found',
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const createdAt = new Date();
|
||||
|
||||
const flatViewGroupsFromOptions: FlatViewGroup[] = (
|
||||
mainGroupByFieldMetadata.options ?? []
|
||||
).map((option, index) => {
|
||||
const viewGroupId = v4();
|
||||
|
||||
return {
|
||||
id: viewGroupId,
|
||||
fieldMetadataId: mainGroupByFieldMetadata.id,
|
||||
viewId: flatViewToCreateId,
|
||||
workspaceId: mainGroupByFieldMetadata.workspaceId,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
universalIdentifier: viewGroupId,
|
||||
isVisible: true,
|
||||
fieldValue: option.value,
|
||||
position: index,
|
||||
applicationId: mainGroupByFieldMetadata.applicationId,
|
||||
};
|
||||
});
|
||||
|
||||
const flatViewGroups: FlatViewGroup[] = [...flatViewGroupsFromOptions];
|
||||
|
||||
if (mainGroupByFieldMetadata.isNullable === true) {
|
||||
const emptyGroupId = v4();
|
||||
|
||||
flatViewGroups.push({
|
||||
id: emptyGroupId,
|
||||
fieldMetadataId: mainGroupByFieldMetadata.id,
|
||||
viewId: flatViewToCreateId,
|
||||
workspaceId: mainGroupByFieldMetadata.workspaceId,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
deletedAt: null,
|
||||
universalIdentifier: emptyGroupId,
|
||||
isVisible: true,
|
||||
fieldValue: '',
|
||||
position: flatViewGroupsFromOptions.length,
|
||||
applicationId: mainGroupByFieldMetadata.applicationId,
|
||||
});
|
||||
}
|
||||
|
||||
return flatViewGroups;
|
||||
};
|
||||
+5
-3
@@ -8,15 +8,17 @@ export const fromCreateViewGroupInputToFlatViewGroupToCreate = ({
|
||||
createViewGroupInput: rawCreateViewGroupInput,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
mainGroupByFieldMetadataId,
|
||||
}: {
|
||||
createViewGroupInput: CreateViewGroupInput;
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
mainGroupByFieldMetadataId: string;
|
||||
}): FlatViewGroup => {
|
||||
const { fieldMetadataId, viewId, ...createViewGroupInput } =
|
||||
const { viewId, ...createViewGroupInput } =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
rawCreateViewGroupInput,
|
||||
['fieldMetadataId', 'fieldValue', 'id', 'viewId'],
|
||||
['fieldValue', 'id', 'viewId'],
|
||||
);
|
||||
|
||||
const createdAt = new Date();
|
||||
@@ -24,7 +26,7 @@ export const fromCreateViewGroupInputToFlatViewGroupToCreate = ({
|
||||
|
||||
return {
|
||||
id: viewGroupId,
|
||||
fieldMetadataId,
|
||||
fieldMetadataId: mainGroupByFieldMetadataId, // Mandatory because non-nullable until we completely remove it
|
||||
viewId,
|
||||
workspaceId,
|
||||
createdAt: createdAt,
|
||||
|
||||
+29
-3
@@ -1,6 +1,12 @@
|
||||
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
|
||||
import {
|
||||
isDefined,
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
|
||||
} from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
|
||||
import { computeFlatViewGroupsOnViewCreate } from 'src/engine/metadata-modules/flat-view-group/utils/compute-flat-view-groups-on-view-create.util';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { type CreateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/create-view.input';
|
||||
import { ViewOpenRecordIn } from 'src/engine/metadata-modules/view/enums/view-open-record-in';
|
||||
@@ -12,12 +18,17 @@ export const fromCreateViewInputToFlatViewToCreate = ({
|
||||
workspaceId,
|
||||
createdByUserWorkspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
flatFieldMetadataMaps,
|
||||
}: {
|
||||
createViewInput: CreateViewInput;
|
||||
workspaceId: string;
|
||||
createdByUserWorkspaceId?: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
}): FlatView => {
|
||||
flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps'];
|
||||
}): {
|
||||
flatViewToCreate: FlatView;
|
||||
flatViewGroupsToCreate: FlatViewGroup[];
|
||||
} => {
|
||||
const { objectMetadataId, ...createViewInput } =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
rawCreateViewInput,
|
||||
@@ -27,7 +38,7 @@ export const fromCreateViewInputToFlatViewToCreate = ({
|
||||
const createdAt = new Date();
|
||||
const viewId = createViewInput.id ?? v4();
|
||||
|
||||
return {
|
||||
const flatViewToCreate = {
|
||||
id: viewId,
|
||||
objectMetadataId,
|
||||
workspaceId,
|
||||
@@ -58,4 +69,19 @@ export const fromCreateViewInputToFlatViewToCreate = ({
|
||||
viewGroupIds: [],
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
};
|
||||
|
||||
let flatViewGroupsToCreate: FlatViewGroup[] = [];
|
||||
|
||||
if (isDefined(flatViewToCreate.mainGroupByFieldMetadataId)) {
|
||||
flatViewGroupsToCreate = computeFlatViewGroupsOnViewCreate({
|
||||
flatViewToCreateId: flatViewToCreate.id,
|
||||
mainGroupByFieldMetadataId: flatViewToCreate.mainGroupByFieldMetadataId,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
flatViewToCreate,
|
||||
flatViewGroupsToCreate,
|
||||
};
|
||||
};
|
||||
|
||||
+43
-2
@@ -5,9 +5,14 @@ import {
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatViewGroupMaps } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group-maps.type';
|
||||
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
|
||||
import { FLAT_VIEW_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view/constants/flat-view-editable-properties.constant';
|
||||
import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { handleFlatViewUpdateSideEffect } from 'src/engine/metadata-modules/flat-view/utils/handle-flat-view-update-side-effect.util';
|
||||
import { type UpdateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/update-view.input';
|
||||
import {
|
||||
ViewException,
|
||||
@@ -18,10 +23,20 @@ import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-
|
||||
export const fromUpdateViewInputToFlatViewToUpdateOrThrow = ({
|
||||
updateViewInput: rawUpdateViewInput,
|
||||
flatViewMaps,
|
||||
flatViewGroupMaps,
|
||||
flatFieldMetadataMaps,
|
||||
userWorkspaceId,
|
||||
}: {
|
||||
updateViewInput: UpdateViewInput;
|
||||
flatViewMaps: FlatViewMaps;
|
||||
}): FlatView => {
|
||||
flatViewGroupMaps: FlatViewGroupMaps;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
userWorkspaceId?: string;
|
||||
}): {
|
||||
flatViewToUpdate: FlatView;
|
||||
flatViewGroupsToDelete: FlatViewGroup[];
|
||||
flatViewGroupsToCreate: FlatViewGroup[];
|
||||
} => {
|
||||
const { id: viewToUpdateId } =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
rawUpdateViewInput,
|
||||
@@ -42,9 +57,35 @@ export const fromUpdateViewInputToFlatViewToUpdateOrThrow = ({
|
||||
FLAT_VIEW_EDITABLE_PROPERTIES,
|
||||
);
|
||||
|
||||
return mergeUpdateInExistingRecord({
|
||||
const flatViewToUpdate = mergeUpdateInExistingRecord({
|
||||
existing: existingFlatViewToUpdate,
|
||||
properties: FLAT_VIEW_EDITABLE_PROPERTIES,
|
||||
update: updatedEditableFieldProperties,
|
||||
});
|
||||
|
||||
// If changing visibility from WORKSPACE to UNLISTED, ensure createdByUserWorkspaceId is set
|
||||
// This prevents the view from disappearing for the user making the change
|
||||
if (
|
||||
isDefined(rawUpdateViewInput.visibility) &&
|
||||
rawUpdateViewInput.visibility === 'UNLISTED' &&
|
||||
existingFlatViewToUpdate.visibility === 'WORKSPACE' &&
|
||||
isDefined(userWorkspaceId)
|
||||
) {
|
||||
// Re-allocate the view to the current user
|
||||
flatViewToUpdate.createdByUserWorkspaceId = userWorkspaceId;
|
||||
}
|
||||
|
||||
const { flatViewGroupsToDelete, flatViewGroupsToCreate } =
|
||||
handleFlatViewUpdateSideEffect({
|
||||
fromFlatView: existingFlatViewToUpdate,
|
||||
toFlatView: flatViewToUpdate,
|
||||
flatViewGroupMaps: flatViewGroupMaps,
|
||||
flatFieldMetadataMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
flatViewToUpdate,
|
||||
flatViewGroupsToDelete,
|
||||
flatViewGroupsToCreate,
|
||||
};
|
||||
};
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { type FromTo } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
|
||||
import { computeFlatViewGroupsOnViewCreate } from 'src/engine/metadata-modules/flat-view-group/utils/compute-flat-view-groups-on-view-create.util';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
|
||||
export type FlatViewUpdateSideEffects = {
|
||||
flatViewGroupsToDelete: FlatViewGroup[];
|
||||
flatViewGroupsToCreate: FlatViewGroup[];
|
||||
};
|
||||
|
||||
type HandleFlatViewUpdateSideEffectArgs = FromTo<FlatView, 'flatView'> &
|
||||
Pick<AllFlatEntityMaps, 'flatViewGroupMaps' | 'flatFieldMetadataMaps'>;
|
||||
|
||||
export const FLAT_VIEW_UPDATE_EMPTY_SIDE_EFFECTS: FlatViewUpdateSideEffects = {
|
||||
flatViewGroupsToDelete: [],
|
||||
flatViewGroupsToCreate: [],
|
||||
};
|
||||
|
||||
export const handleFlatViewUpdateSideEffect = ({
|
||||
fromFlatView,
|
||||
toFlatView,
|
||||
flatViewGroupMaps,
|
||||
flatFieldMetadataMaps,
|
||||
}: HandleFlatViewUpdateSideEffectArgs): FlatViewUpdateSideEffects => {
|
||||
const sideEffectResult = structuredClone(FLAT_VIEW_UPDATE_EMPTY_SIDE_EFFECTS);
|
||||
|
||||
const newMainGroupByFieldMetadataId = toFlatView.mainGroupByFieldMetadataId;
|
||||
|
||||
const hasMainGroupByFieldMetadataIdChanged =
|
||||
fromFlatView.mainGroupByFieldMetadataId !== newMainGroupByFieldMetadataId;
|
||||
|
||||
if (!hasMainGroupByFieldMetadataIdChanged) {
|
||||
return sideEffectResult;
|
||||
}
|
||||
|
||||
if (fromFlatView.viewGroupIds.length > 0) {
|
||||
sideEffectResult.flatViewGroupsToDelete =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: fromFlatView.viewGroupIds,
|
||||
flatEntityMaps: flatViewGroupMaps,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isDefined(newMainGroupByFieldMetadataId)) {
|
||||
return sideEffectResult;
|
||||
}
|
||||
|
||||
sideEffectResult.flatViewGroupsToCreate = computeFlatViewGroupsOnViewCreate({
|
||||
flatViewToCreateId: toFlatView.id,
|
||||
mainGroupByFieldMetadataId: newMainGroupByFieldMetadataId,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
return sideEffectResult;
|
||||
};
|
||||
+7
-2
@@ -7,6 +7,7 @@ import { FindManyOptions, FindOneOptions, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.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 { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
@@ -385,6 +386,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
objectMetadata: flatObjectMetadataToCreate,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
flatFieldMetadataMaps: flatFieldMetadataMapsFromTo.to,
|
||||
});
|
||||
|
||||
const flatDefaultViewFieldsToCreate =
|
||||
@@ -476,10 +478,12 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
objectMetadata,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
flatFieldMetadataMaps,
|
||||
}: {
|
||||
workspaceCustomApplicationId: string;
|
||||
objectMetadata: FlatObjectMetadata;
|
||||
workspaceId: string;
|
||||
flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps'];
|
||||
}) {
|
||||
const defaultViewInput = {
|
||||
objectMetadataId: objectMetadata.id,
|
||||
@@ -490,13 +494,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
workspaceId: workspaceId,
|
||||
};
|
||||
|
||||
const flatViewFromCreateInput = fromCreateViewInputToFlatViewToCreate({
|
||||
const { flatViewToCreate } = fromCreateViewInputToFlatViewToCreate({
|
||||
createViewInput: defaultViewInput,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
return flatViewFromCreateInput;
|
||||
return flatViewToCreate;
|
||||
}
|
||||
|
||||
private async computeFlatViewFieldsToCreate({
|
||||
|
||||
+6
-14
@@ -28,7 +28,6 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
type: FieldMetadataType.UUID,
|
||||
id: idFieldId,
|
||||
viewFieldIds: [],
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
@@ -66,10 +65,9 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
type: FieldMetadataType.TEXT,
|
||||
id: nameFieldId,
|
||||
viewFieldIds: [],
|
||||
viewGroupIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
@@ -104,10 +102,9 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
id: createdAtFieldId,
|
||||
viewFieldIds: [],
|
||||
viewGroupIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
@@ -142,10 +139,9 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
id: updatedAtFieldId,
|
||||
viewFieldIds: [],
|
||||
viewGroupIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
@@ -180,10 +176,9 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
id: deletedAtFieldId,
|
||||
viewFieldIds: [],
|
||||
viewGroupIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
@@ -218,10 +213,9 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
type: FieldMetadataType.ACTOR,
|
||||
id: createdByFieldId,
|
||||
viewFieldIds: [],
|
||||
viewGroupIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
@@ -255,10 +249,9 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
type: FieldMetadataType.POSITION,
|
||||
id: positionFieldId,
|
||||
viewFieldIds: [],
|
||||
viewGroupIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
@@ -293,7 +286,6 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
viewFieldIds: [],
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
id: searchVectorFieldId,
|
||||
|
||||
-2
@@ -65,7 +65,6 @@ const generateSourceFlatFieldMetadata = ({
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
viewFilterIds: [],
|
||||
viewFieldIds: [],
|
||||
viewGroupIds: [],
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
defaultValue: null,
|
||||
@@ -133,7 +132,6 @@ const generateTargetFlatFieldMetadata = ({
|
||||
viewFieldIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
viewFilterIds: [],
|
||||
viewGroupIds: [],
|
||||
id: sourceFlatFieldMetadata.relationTargetFieldMetadataId,
|
||||
name: sourceFlatObjectMetadata.nameSingular,
|
||||
label: sourceFlatObjectMetadata.labelSingular,
|
||||
|
||||
-4
@@ -17,10 +17,6 @@ export class CreateViewGroupInput {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
id?: string;
|
||||
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
fieldMetadataId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field({ nullable: true, defaultValue: true })
|
||||
|
||||
@@ -9,9 +9,6 @@ export class ViewGroupDTO {
|
||||
@IDField(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
fieldMetadataId: string;
|
||||
|
||||
@Field({ nullable: false, defaultValue: true })
|
||||
isVisible: boolean;
|
||||
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ export enum ViewGroupExceptionCode {
|
||||
VIEW_GROUP_NOT_FOUND = 'VIEW_GROUP_NOT_FOUND',
|
||||
INVALID_VIEW_GROUP_DATA = 'INVALID_VIEW_GROUP_DATA',
|
||||
VIEW_NOT_FOUND = 'VIEW_NOT_FOUND',
|
||||
MISSING_MAIN_GROUP_BY_FIELD_METADATA_ID = 'MISSING_MAIN_GROUP_BY_FIELD_METADATA_ID',
|
||||
}
|
||||
|
||||
export enum ViewGroupExceptionMessageKey {
|
||||
|
||||
+2
-2
@@ -11,14 +11,14 @@ import { DestroyViewGroupInput } from 'src/engine/metadata-modules/view-group/dt
|
||||
import { UpdateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/update-view-group.input';
|
||||
import { ViewGroupDTO } from 'src/engine/metadata-modules/view-group/dtos/view-group.dto';
|
||||
import { ViewGroupService } from 'src/engine/metadata-modules/view-group/services/view-group.service';
|
||||
import { ViewGroupGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view-group/utils/view-group-graphql-api-exception.filter';
|
||||
import { CreateViewGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-group-permission.guard';
|
||||
import { DeleteViewGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-group-permission.guard';
|
||||
import { DestroyViewGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/destroy-view-group-permission.guard';
|
||||
import { UpdateViewGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-group-permission.guard';
|
||||
import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter';
|
||||
|
||||
@Resolver(() => ViewGroupDTO)
|
||||
@UseFilters(ViewGraphqlApiExceptionFilter)
|
||||
@UseFilters(ViewGroupGraphqlApiExceptionFilter)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
export class ViewGroupResolver {
|
||||
constructor(private readonly viewGroupService: ViewGroupService) {}
|
||||
|
||||
+16
-3
@@ -92,12 +92,25 @@ export class ViewGroupService {
|
||||
);
|
||||
|
||||
const flatViewGroupsToCreate = createViewGroupInputs.map(
|
||||
(createViewGroupInput) =>
|
||||
fromCreateViewGroupInputToFlatViewGroupToCreate({
|
||||
(createViewGroupInput) => {
|
||||
const mainGroupByFieldMetadataId =
|
||||
flatViewMaps.byId[createViewGroupInput.viewId]
|
||||
?.mainGroupByFieldMetadataId;
|
||||
|
||||
if (!isDefined(mainGroupByFieldMetadataId)) {
|
||||
throw new ViewGroupException(
|
||||
'The associated view is not a grouped view: mainGroupByFieldMetadataId is missing.',
|
||||
ViewGroupExceptionCode.MISSING_MAIN_GROUP_BY_FIELD_METADATA_ID,
|
||||
);
|
||||
}
|
||||
|
||||
return fromCreateViewGroupInputToFlatViewGroupToCreate({
|
||||
createViewGroupInput,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
}),
|
||||
mainGroupByFieldMetadataId,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const validateAndBuildResult =
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
ViewGroupException,
|
||||
ViewGroupExceptionCode,
|
||||
} from 'src/engine/metadata-modules/view-group/exceptions/view-group.exception';
|
||||
import { viewGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception-handler.util';
|
||||
|
||||
export const viewGroupGraphqlApiExceptionHandler = (
|
||||
error: Error,
|
||||
i18n: I18n,
|
||||
) => {
|
||||
if (error instanceof ViewGroupException) {
|
||||
if (
|
||||
error.code ===
|
||||
ViewGroupExceptionCode.MISSING_MAIN_GROUP_BY_FIELD_METADATA_ID
|
||||
) {
|
||||
throw new UserInputError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return viewGraphqlApiExceptionHandler(error, i18n);
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
Catch,
|
||||
type ExceptionFilter,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { ViewGroupException } from 'src/engine/metadata-modules/view-group/exceptions/view-group.exception';
|
||||
import { viewGroupGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/view-group/utils/view-group-graphql-api-exception-handler.util';
|
||||
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
|
||||
@Catch(ViewGroupException, WorkspaceMigrationBuilderExceptionV2)
|
||||
@Injectable()
|
||||
export class ViewGroupGraphqlApiExceptionFilter implements ExceptionFilter {
|
||||
constructor(private readonly i18nService: I18nService) {}
|
||||
|
||||
catch(
|
||||
exception: ViewGroupException | WorkspaceMigrationBuilderExceptionV2,
|
||||
host: ExecutionContext,
|
||||
) {
|
||||
const gqlContext = GqlExecutionContext.create(host);
|
||||
const ctx = gqlContext.getContext();
|
||||
const userLocale = ctx.req?.locale ?? SOURCE_LOCALE;
|
||||
const i18n = this.i18nService.getI18nInstance(userLocale);
|
||||
|
||||
return viewGroupGraphqlApiExceptionHandler(exception, i18n);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { I18nModule } from 'src/engine/core-modules/i18n/i18n.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 { ViewGroupController } from 'src/engine/metadata-modules/view-group/controllers/view-group.controller';
|
||||
@@ -20,6 +21,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
|
||||
WorkspaceCacheStorageModule,
|
||||
ApplicationModule,
|
||||
FeatureFlagModule,
|
||||
I18nModule,
|
||||
PermissionsModule,
|
||||
WorkspaceMigrationV2Module,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
|
||||
+1
-1
@@ -91,5 +91,5 @@ export class UpdateViewInput {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
mainGroupByFieldMetadataId?: string;
|
||||
mainGroupByFieldMetadataId?: string | null;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
@@ -56,6 +56,7 @@ export class ViewService {
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatViewGroupMaps: existingFlatViewGroupMaps,
|
||||
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
@@ -63,16 +64,19 @@ export class ViewService {
|
||||
'flatObjectMetadataMaps',
|
||||
'flatViewMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatViewGroupMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const flatViewFromCreateInput = fromCreateViewInputToFlatViewToCreate({
|
||||
createViewInput,
|
||||
workspaceId,
|
||||
createdByUserWorkspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
const { flatViewToCreate, flatViewGroupsToCreate } =
|
||||
fromCreateViewInputToFlatViewToCreate({
|
||||
createViewInput,
|
||||
workspaceId,
|
||||
createdByUserWorkspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
@@ -80,7 +84,13 @@ export class ViewService {
|
||||
fromToAllFlatEntityMaps: {
|
||||
flatViewMaps: computeFlatEntityMapsFromTo({
|
||||
flatEntityMaps: existingFlatViewMaps,
|
||||
flatEntityToCreate: [flatViewFromCreateInput],
|
||||
flatEntityToCreate: [flatViewToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
}),
|
||||
flatViewGroupMaps: computeFlatEntityMapsFromTo({
|
||||
flatEntityMaps: existingFlatViewGroupMaps,
|
||||
flatEntityToCreate: flatViewGroupsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
}),
|
||||
@@ -112,7 +122,7 @@ export class ViewService {
|
||||
);
|
||||
|
||||
return findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: flatViewFromCreateInput.id,
|
||||
flatEntityId: flatViewToCreate.id,
|
||||
flatEntityMaps: recomputedExistingFlatViewMaps,
|
||||
});
|
||||
}
|
||||
@@ -129,35 +139,27 @@ export class ViewService {
|
||||
const {
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
} =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatViewMaps', 'flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
flatViewGroupMaps: existingFlatViewGroupMaps,
|
||||
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatViewMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatViewGroupMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const flatViewFromUpdateInput =
|
||||
const { flatViewToUpdate, flatViewGroupsToDelete, flatViewGroupsToCreate } =
|
||||
fromUpdateViewInputToFlatViewToUpdateOrThrow({
|
||||
updateViewInput,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatViewGroupMaps: existingFlatViewGroupMaps,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
userWorkspaceId,
|
||||
});
|
||||
|
||||
const existingFlatView = existingFlatViewMaps.byId[updateViewInput.id];
|
||||
|
||||
// If changing visibility from WORKSPACE to UNLISTED, ensure createdByUserWorkspaceId is set
|
||||
// This prevents the view from disappearing for the user making the change
|
||||
if (
|
||||
isDefined(existingFlatView) &&
|
||||
isDefined(updateViewInput.visibility) &&
|
||||
updateViewInput.visibility === 'UNLISTED' &&
|
||||
existingFlatView.visibility === 'WORKSPACE' &&
|
||||
isDefined(userWorkspaceId)
|
||||
) {
|
||||
// Re-allocate the view to the current user
|
||||
flatViewFromUpdateInput.createdByUserWorkspaceId = userWorkspaceId;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
@@ -166,7 +168,13 @@ export class ViewService {
|
||||
flatEntityMaps: existingFlatViewMaps,
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatViewFromUpdateInput],
|
||||
flatEntityToUpdate: [flatViewToUpdate],
|
||||
}),
|
||||
flatViewGroupMaps: computeFlatEntityMapsFromTo({
|
||||
flatEntityMaps: existingFlatViewGroupMaps,
|
||||
flatEntityToCreate: flatViewGroupsToCreate,
|
||||
flatEntityToDelete: flatViewGroupsToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
}),
|
||||
},
|
||||
dependencyAllFlatEntityMaps: {
|
||||
@@ -174,6 +182,9 @@ export class ViewService {
|
||||
},
|
||||
buildOptions: {
|
||||
isSystemBuild: false,
|
||||
inferDeletionFromMissingEntities: {
|
||||
viewGroup: true,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
},
|
||||
@@ -385,7 +396,6 @@ export class ViewService {
|
||||
if (view.visibility === ViewVisibility.WORKSPACE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
view.visibility === ViewVisibility.UNLISTED &&
|
||||
isDefined(userWorkspaceId) &&
|
||||
@@ -425,7 +435,6 @@ export class ViewService {
|
||||
if (view.visibility === ViewVisibility.WORKSPACE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
view.visibility === ViewVisibility.UNLISTED &&
|
||||
isDefined(userWorkspaceId) &&
|
||||
|
||||
+5
@@ -3,6 +3,7 @@ import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ForbiddenError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
@@ -118,6 +119,10 @@ export const viewGraphqlApiExceptionHandler = (error: Error, i18n: I18n) => {
|
||||
throw new UserInputError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
case ViewGroupExceptionCode.MISSING_MAIN_GROUP_BY_FIELD_METADATA_ID:
|
||||
throw new InternalServerError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user