Add syncable NavigationMenuItem entity to core schema (#17232)

Implements a new syncable `navigationMenuItem` entity in the core schema
to replace the workspace `favorite` entity.

## Next Steps
- Frontend integration ([separate
PR](https://github.com/twentyhq/twenty/pull/17268))
- Data migration (separate PR)
This commit is contained in:
Abdul Rahman
2026-01-22 18:18:51 +05:30
committed by GitHub
parent 2cda1f5f08
commit aff577689f
71 changed files with 4050 additions and 3 deletions
@@ -21,6 +21,7 @@ import { WorkspaceMigrationDatabaseEventTriggerActionsBuilderService } from 'src
import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service';
import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service';
import { WorkspaceMigrationIndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/workspace-migration-index-actions-builder.service';
import { WorkspaceMigrationNavigationMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/workspace-migration-navigation-menu-item-actions-builder.service';
import { WorkspaceMigrationObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/workspace-migration-object-actions-builder.service';
import { WorkspaceMigrationPageLayoutTabActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-tab/workspace-migration-page-layout-tab-actions-builder.service';
import { WorkspaceMigrationPageLayoutWidgetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/workspace-migration-page-layout-widget-actions-builder.service';
@@ -58,6 +59,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
private readonly workspaceMigrationAgentActionsBuilderService: WorkspaceMigrationAgentActionsBuilderService,
private readonly workspaceMigrationSkillActionsBuilderService: WorkspaceMigrationSkillActionsBuilderService,
private readonly workspaceMigrationCommandMenuItemActionsBuilderService: WorkspaceMigrationCommandMenuItemActionsBuilderService,
private readonly workspaceMigrationNavigationMenuItemActionsBuilderService: WorkspaceMigrationNavigationMenuItemActionsBuilderService,
private readonly workspaceMigrationPageLayoutActionsBuilderService: WorkspaceMigrationPageLayoutActionsBuilderService,
private readonly workspaceMigrationPageLayoutWidgetActionsBuilderService: WorkspaceMigrationPageLayoutWidgetActionsBuilderService,
private readonly workspaceMigrationPageLayoutTabActionsBuilderService: WorkspaceMigrationPageLayoutTabActionsBuilderService,
@@ -161,6 +163,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
flatAgentMaps,
flatSkillMaps,
flatCommandMenuItemMaps,
flatNavigationMenuItemMaps,
flatPageLayoutMaps,
flatPageLayoutWidgetMaps,
flatPageLayoutTabMaps,
@@ -859,6 +862,46 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
if (isDefined(flatNavigationMenuItemMaps)) {
const {
from: fromFlatNavigationMenuItemMaps,
to: toFlatNavigationMenuItemMaps,
} = flatNavigationMenuItemMaps;
const navigationMenuItemResult =
await this.workspaceMigrationNavigationMenuItemActionsBuilderService.validateAndBuild(
{
additionalCacheDataMaps,
from: fromFlatNavigationMenuItemMaps,
to: toFlatNavigationMenuItemMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: {
flatObjectMetadataMaps:
optimisticAllFlatEntityMaps.flatObjectMetadataMaps,
flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps,
},
workspaceId,
},
);
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
{
allFlatEntityMaps: optimisticAllFlatEntityMaps,
flatEntityMapsAndRelatedFlatEntityMaps:
navigationMenuItemResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
},
);
if (navigationMenuItemResult.status === 'fail') {
orchestratorFailureReport.navigationMenuItem.push(
...navigationMenuItemResult.errors,
);
} else {
orchestratorActionsReport.navigationMenuItem =
navigationMenuItemResult.actions;
}
}
if (isDefined(flatPageLayoutMaps)) {
const { from: fromFlatPageLayoutMaps, to: toFlatPageLayoutMaps } =
flatPageLayoutMaps;
@@ -1118,6 +1161,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
...aggregatedOrchestratorActionsReport.commandMenuItem.update,
///
// Navigation Menu Items
...aggregatedOrchestratorActionsReport.navigationMenuItem.delete,
...aggregatedOrchestratorActionsReport.navigationMenuItem.create,
...aggregatedOrchestratorActionsReport.navigationMenuItem.update,
///
// Page layouts
...aggregatedOrchestratorActionsReport.pageLayout.delete,
...aggregatedOrchestratorActionsReport.pageLayout.create,
@@ -0,0 +1,12 @@
import { type BaseCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-create-workspace-migration-action.type';
import { type BaseDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-delete-workspace-migration-action.type';
import { type BaseUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-update-workspace-migration-action.type';
export type CreateNavigationMenuItemAction =
BaseCreateWorkspaceMigrationAction<'navigationMenuItem'>;
export type UpdateNavigationMenuItemAction =
BaseUpdateWorkspaceMigrationAction<'navigationMenuItem'>;
export type DeleteNavigationMenuItemAction =
BaseDeleteWorkspaceMigrationAction<'navigationMenuItem'>;
@@ -0,0 +1,117 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { UpdateNavigationMenuItemAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/types/workspace-migration-navigation-menu-item-action.type';
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-update-validation-args.type';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-args.type';
import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-result.type';
import { FlatNavigationMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-navigation-menu-item-validator.service';
@Injectable()
export class WorkspaceMigrationNavigationMenuItemActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
typeof ALL_METADATA_NAME.navigationMenuItem
> {
constructor(
private readonly flatNavigationMenuItemValidatorService: FlatNavigationMenuItemValidatorService,
) {
super(ALL_METADATA_NAME.navigationMenuItem);
}
protected validateFlatEntityCreation(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.navigationMenuItem>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.navigationMenuItem,
'create'
> {
const validationResult =
this.flatNavigationMenuItemValidatorService.validateFlatNavigationMenuItemCreation(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatNavigationMenuItemToValidate } = args;
return {
status: 'success',
action: {
type: 'create',
metadataName: 'navigationMenuItem',
flatEntity: flatNavigationMenuItemToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.navigationMenuItem>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.navigationMenuItem,
'delete'
> {
const validationResult =
this.flatNavigationMenuItemValidatorService.validateFlatNavigationMenuItemDeletion(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatNavigationMenuItemToValidate } = args;
return {
status: 'success',
action: {
type: 'delete',
metadataName: 'navigationMenuItem',
universalIdentifier:
flatNavigationMenuItemToValidate.universalIdentifier,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.navigationMenuItem
>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.navigationMenuItem,
'update'
> {
const validationResult =
this.flatNavigationMenuItemValidatorService.validateFlatNavigationMenuItemUpdate(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityId, flatEntityUpdates } = args;
const updateNavigationMenuItemAction: UpdateNavigationMenuItemAction = {
type: 'update',
metadataName: 'navigationMenuItem',
entityId: flatEntityId,
updates: flatEntityUpdates,
};
return {
status: 'success',
action: updateNavigationMenuItemAction,
};
}
}
@@ -0,0 +1,382 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
import { NavigationMenuItemExceptionCode } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.exception';
import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/utils/find-flat-entity-property-update.util';
import { validateFlatEntityCircularDependency } from 'src/engine/workspace-manager/workspace-migration/utils/validate-flat-entity-circular-dependency.util';
import {
type FailedFlatEntityValidation,
type FlatEntityValidationError,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-update-validation-args.type';
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-args.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
const NAVIGATION_MENU_ITEM_MAX_DEPTH = 2;
@Injectable()
export class FlatNavigationMenuItemValidatorService {
private validateNavigationMenuItemType({
hasTargetRecordId,
hasTargetObjectMetadataId,
hasViewId,
name,
isUpdate = false,
}: {
hasTargetRecordId: boolean;
hasTargetObjectMetadataId: boolean;
hasViewId: boolean;
name: string | null | undefined;
isUpdate?: boolean;
}): FlatEntityValidationError<NavigationMenuItemExceptionCode>[] {
const errors: FlatEntityValidationError<NavigationMenuItemExceptionCode>[] =
[];
if (hasTargetObjectMetadataId && !hasTargetRecordId) {
errors.push({
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
message: t`targetRecordId is required when targetObjectMetadataId is provided`,
userFriendlyMessage: msg`targetRecordId is required when targetObjectMetadataId is provided`,
});
}
if (hasTargetRecordId && !hasTargetObjectMetadataId) {
errors.push({
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
message: t`targetObjectMetadataId is required when targetRecordId is provided`,
userFriendlyMessage: msg`targetObjectMetadataId is required when targetRecordId is provided`,
});
}
if (hasViewId && (hasTargetRecordId || hasTargetObjectMetadataId)) {
errors.push({
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
message: t`viewId cannot be provided together with targetRecordId or targetObjectMetadataId`,
userFriendlyMessage: msg`viewId cannot be provided together with targetRecordId or targetObjectMetadataId`,
});
}
const isFolder =
!hasTargetRecordId && !hasTargetObjectMetadataId && !hasViewId;
const isViewLink = hasViewId;
const isRecordLink = hasTargetRecordId && hasTargetObjectMetadataId;
const typeCount =
(isFolder ? 1 : 0) + (isViewLink ? 1 : 0) + (isRecordLink ? 1 : 0);
if (typeCount === 0) {
errors.push({
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
message: t`Navigation menu item must be either a folder (with name), a view link (with viewId), or a record link (with targetRecordId and targetObjectMetadataId)`,
userFriendlyMessage: msg`Navigation menu item must be either a folder (with name), a view link (with viewId), or a record link (with targetRecordId and targetObjectMetadataId)`,
});
}
if (typeCount > 1) {
errors.push({
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
message: t`Navigation menu item cannot be multiple types simultaneously`,
userFriendlyMessage: msg`Navigation menu item cannot be multiple types simultaneously`,
});
}
if (isFolder && (!isDefined(name) || name.trim() === '')) {
errors.push({
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
message: isUpdate
? t`Folder name is required and cannot be empty`
: t`Folder name is required when creating a folder`,
userFriendlyMessage: isUpdate
? msg`Folder name is required and cannot be empty`
: msg`Folder name is required when creating a folder`,
});
}
return errors;
}
private getCircularDependencyValidationErrors({
navigationMenuItemId,
folderId,
flatNavigationMenuItemMaps,
}: {
navigationMenuItemId: string;
folderId: string;
flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
}): FlatEntityValidationError<NavigationMenuItemExceptionCode>[] {
const circularDependencyResult = validateFlatEntityCircularDependency({
flatEntityId: navigationMenuItemId,
flatEntityParentId: folderId,
maxDepth: NAVIGATION_MENU_ITEM_MAX_DEPTH,
parentIdKey: 'folderId',
flatEntityMaps: flatNavigationMenuItemMaps,
});
if (circularDependencyResult.status === 'success') {
return [];
}
switch (circularDependencyResult.reason) {
case 'self_reference':
return [
{
code: NavigationMenuItemExceptionCode.CIRCULAR_DEPENDENCY,
message: t`Navigation menu item cannot be its own parent`,
userFriendlyMessage: msg`Navigation menu item cannot be its own parent`,
},
];
case 'circular_dependency':
return [
{
code: NavigationMenuItemExceptionCode.CIRCULAR_DEPENDENCY,
message: t`Circular dependency detected in navigation menu item hierarchy`,
userFriendlyMessage: msg`Circular dependency detected in navigation menu item hierarchy`,
},
];
case 'max_depth_exceeded':
return [
{
code: NavigationMenuItemExceptionCode.MAX_DEPTH_EXCEEDED,
message: t`Navigation menu item hierarchy exceeds maximum depth of ${NAVIGATION_MENU_ITEM_MAX_DEPTH}`,
userFriendlyMessage: msg`Navigation menu item hierarchy exceeds maximum depth of ${NAVIGATION_MENU_ITEM_MAX_DEPTH}`,
},
];
}
}
public validateFlatNavigationMenuItemCreation({
flatEntityToValidate: flatNavigationMenuItem,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps,
},
remainingFlatEntityMapsToValidate,
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.navigationMenuItem
>): FailedFlatEntityValidation<'navigationMenuItem', 'create'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
id: flatNavigationMenuItem.id,
universalIdentifier: flatNavigationMenuItem.universalIdentifier,
},
metadataName: 'navigationMenuItem',
type: 'create',
});
if (
isDefined(flatNavigationMenuItem.position) &&
(!Number.isInteger(flatNavigationMenuItem.position) ||
flatNavigationMenuItem.position < 0)
) {
validationResult.errors.push({
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
message: t`Position must be a non-negative integer`,
userFriendlyMessage: msg`Position must be a non-negative integer`,
});
}
const typeValidationErrors = this.validateNavigationMenuItemType({
hasTargetRecordId: isDefined(flatNavigationMenuItem.targetRecordId),
hasTargetObjectMetadataId: isDefined(
flatNavigationMenuItem.targetObjectMetadataId,
),
hasViewId: isDefined(flatNavigationMenuItem.viewId),
name: flatNavigationMenuItem.name,
isUpdate: false,
});
validationResult.errors.push(...typeValidationErrors);
if (isDefined(flatNavigationMenuItem.folderId)) {
const circularDependencyErrors =
this.getCircularDependencyValidationErrors({
navigationMenuItemId: flatNavigationMenuItem.id,
folderId: flatNavigationMenuItem.folderId,
flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps,
});
if (circularDependencyErrors.length > 0) {
validationResult.errors.push(...circularDependencyErrors);
}
const referencedParentInOptimistic = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatNavigationMenuItem.folderId,
flatEntityMaps: optimisticFlatNavigationMenuItemMaps,
});
const referencedParentInRemaining = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatNavigationMenuItem.folderId,
flatEntityMaps: remainingFlatEntityMapsToValidate,
});
if (
!isDefined(referencedParentInOptimistic) &&
!isDefined(referencedParentInRemaining)
) {
validationResult.errors.push({
code: NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND,
message: t`Parent navigation menu item not found`,
userFriendlyMessage: msg`Parent navigation menu item not found`,
});
}
}
return validationResult;
}
public validateFlatNavigationMenuItemDeletion({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.navigationMenuItem
>): FailedFlatEntityValidation<'navigationMenuItem', 'delete'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
id: flatEntityToValidate.id,
universalIdentifier: flatEntityToValidate.universalIdentifier,
},
metadataName: 'navigationMenuItem',
type: 'delete',
});
const existingNavigationMenuItem = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatEntityToValidate.id,
flatEntityMaps: optimisticFlatNavigationMenuItemMaps,
});
if (!isDefined(existingNavigationMenuItem)) {
validationResult.errors.push({
code: NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND,
message: t`Navigation menu item not found`,
userFriendlyMessage: msg`Navigation menu item not found`,
});
}
return validationResult;
}
public validateFlatNavigationMenuItemUpdate({
flatEntityId,
flatEntityUpdates,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps,
},
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.navigationMenuItem
>): FailedFlatEntityValidation<'navigationMenuItem', 'update'> {
const fromFlatNavigationMenuItem = findFlatEntityByIdInFlatEntityMaps({
flatEntityId,
flatEntityMaps: optimisticFlatNavigationMenuItemMaps,
});
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
id: flatEntityId,
universalIdentifier: fromFlatNavigationMenuItem?.universalIdentifier,
},
metadataName: 'navigationMenuItem',
type: 'update',
});
if (!isDefined(fromFlatNavigationMenuItem)) {
validationResult.errors.push({
code: NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND,
message: t`Navigation menu item not found`,
userFriendlyMessage: msg`Navigation menu item not found`,
});
return validationResult;
}
const positionUpdate = findFlatEntityPropertyUpdate({
flatEntityUpdates,
property: 'position',
});
if (
isDefined(positionUpdate) &&
(!Number.isInteger(positionUpdate.to) || positionUpdate.to < 0)
) {
validationResult.errors.push({
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
message: t`Position must be a non-negative integer`,
userFriendlyMessage: msg`Position must be a non-negative integer`,
});
}
const toFlatNavigationMenuItem = {
...fromFlatNavigationMenuItem,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates: flatEntityUpdates,
}),
};
const nameUpdate = findFlatEntityPropertyUpdate({
flatEntityUpdates,
property: 'name',
});
const typeValidationErrors = this.validateNavigationMenuItemType({
hasTargetRecordId: isDefined(toFlatNavigationMenuItem.targetRecordId),
hasTargetObjectMetadataId: isDefined(
toFlatNavigationMenuItem.targetObjectMetadataId,
),
hasViewId: isDefined(toFlatNavigationMenuItem.viewId),
name: isDefined(nameUpdate)
? nameUpdate.to
: toFlatNavigationMenuItem.name,
isUpdate: true,
});
validationResult.errors.push(...typeValidationErrors);
const folderIdUpdate = findFlatEntityPropertyUpdate({
flatEntityUpdates,
property: 'folderId',
});
if (!isDefined(folderIdUpdate)) {
return validationResult;
}
const newFolderId = folderIdUpdate.to;
if (!isDefined(newFolderId)) {
return validationResult;
}
const circularDependencyErrors = this.getCircularDependencyValidationErrors(
{
navigationMenuItemId: flatEntityId,
folderId: newFolderId,
flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps,
},
);
if (circularDependencyErrors.length > 0) {
validationResult.errors.push(...circularDependencyErrors);
}
const referencedParentNavigationMenuItem =
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: newFolderId,
flatEntityMaps: optimisticFlatNavigationMenuItemMaps,
});
if (!isDefined(referencedParentNavigationMenuItem)) {
validationResult.errors.push({
code: NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND,
message: t`Parent navigation menu item not found`,
userFriendlyMessage: msg`Parent navigation menu item not found`,
});
}
return validationResult;
}
}
@@ -4,10 +4,13 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
import { FlatAgentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-agent-validator.service';
import { FlatCommandMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-command-menu-item-validator.service';
import { FlatCronTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-cron-trigger-validator.service';
import { FlatDatabaseEventTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-database-event-trigger-validator.service';
import { FlatFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-field-metadata-validator.service';
import { FlatFrontComponentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service';
import { FlatIndexValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-index-metadata-validator.service';
import { FlatNavigationMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-navigation-menu-item-validator.service';
import { FlatObjectMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-metadata-validator.service';
import { FlatPageLayoutTabValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service';
import { FlatPageLayoutValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-validator.service';
@@ -18,9 +21,7 @@ import { FlatRouteTriggerValidatorService } from 'src/engine/workspace-manager/w
import { FlatRowLevelPermissionPredicateGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-row-level-permission-predicate-group-validator.service';
import { FlatRowLevelPermissionPredicateValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-row-level-permission-predicate-validator.service';
import { FlatServerlessFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-serverless-function-validator.service';
import { FlatCommandMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-command-menu-item-validator.service';
import { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-skill-validator.service';
import { FlatFrontComponentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service';
import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-validator.service';
import { FlatViewFilterGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-group-validator.service';
import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service';
@@ -49,6 +50,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatAgentValidatorService,
FlatSkillValidatorService,
FlatCommandMenuItemValidatorService,
FlatNavigationMenuItemValidatorService,
FlatPageLayoutValidatorService,
FlatPageLayoutWidgetValidatorService,
FlatPageLayoutTabValidatorService,
@@ -75,6 +77,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatAgentValidatorService,
FlatSkillValidatorService,
FlatCommandMenuItemValidatorService,
FlatNavigationMenuItemValidatorService,
FlatPageLayoutValidatorService,
FlatPageLayoutWidgetValidatorService,
FlatPageLayoutTabValidatorService,
@@ -4,6 +4,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/agent/workspace-migration-agent-actions-builder.service';
import { WorkspaceMigrationCommandMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/workspace-migration-command-menu-item-actions-builder.service';
import { WorkspaceMigrationNavigationMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/workspace-migration-navigation-menu-item-actions-builder.service';
import { WorkspaceMigrationCronTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/cron-trigger/workspace-migration-cron-trigger-action-builder.service';
import { WorkspaceMigrationDatabaseEventTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/database-event-trigger/workspace-migration-database-event-trigger-actions-builder.service';
import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service';
@@ -48,6 +49,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationAgentActionsBuilderService,
WorkspaceMigrationSkillActionsBuilderService,
WorkspaceMigrationCommandMenuItemActionsBuilderService,
WorkspaceMigrationNavigationMenuItemActionsBuilderService,
WorkspaceMigrationPageLayoutActionsBuilderService,
WorkspaceMigrationPageLayoutWidgetActionsBuilderService,
WorkspaceMigrationPageLayoutTabActionsBuilderService,
@@ -73,6 +75,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationAgentActionsBuilderService,
WorkspaceMigrationSkillActionsBuilderService,
WorkspaceMigrationCommandMenuItemActionsBuilderService,
WorkspaceMigrationNavigationMenuItemActionsBuilderService,
WorkspaceMigrationPageLayoutActionsBuilderService,
WorkspaceMigrationPageLayoutWidgetActionsBuilderService,
WorkspaceMigrationPageLayoutTabActionsBuilderService,
@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
import { CreateNavigationMenuItemAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/types/workspace-migration-navigation-menu-item-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateNavigationMenuItemActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create',
'navigationMenuItem',
) {
constructor() {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateNavigationMenuItemAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { flatEntity } = action;
const navigationMenuItemRepository =
queryRunner.manager.getRepository<NavigationMenuItemEntity>(
NavigationMenuItemEntity,
);
await navigationMenuItemRepository.insert({
...flatEntity,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<CreateNavigationMenuItemAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,46 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
import { DeleteNavigationMenuItemAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/types/workspace-migration-navigation-menu-item-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteNavigationMenuItemActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete',
'navigationMenuItem',
) {
constructor() {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteNavigationMenuItemAction>,
): Promise<void> {
const { action, queryRunner, workspaceId, allFlatEntityMaps } = context;
const { universalIdentifier } = action;
const flatNavigationMenuItem = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: allFlatEntityMaps.flatNavigationMenuItemMaps,
universalIdentifier,
});
const navigationMenuItemRepository =
queryRunner.manager.getRepository<NavigationMenuItemEntity>(
NavigationMenuItemEntity,
);
await navigationMenuItemRepository.delete({
id: flatNavigationMenuItem.id,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteNavigationMenuItemAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
import { UpdateNavigationMenuItemAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/types/workspace-migration-navigation-menu-item-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class UpdateNavigationMenuItemActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update',
'navigationMenuItem',
) {
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateNavigationMenuItemAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { entityId, updates } = action;
const navigationMenuItemRepository =
queryRunner.manager.getRepository<NavigationMenuItemEntity>(
NavigationMenuItemEntity,
);
await navigationMenuItemRepository.update(
{ id: entityId, workspaceId },
fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates,
}),
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<UpdateNavigationMenuItemAction>,
): Promise<void> {
return;
}
}
@@ -7,6 +7,9 @@ import { UpdateAgentActionHandlerService } from 'src/engine/workspace-manager/wo
import { CreateCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/create-command-menu-item-action-handler.service';
import { DeleteCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/delete-command-menu-item-action-handler.service';
import { UpdateCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/update-command-menu-item-action-handler.service';
import { CreateNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/create-navigation-menu-item-action-handler.service';
import { DeleteNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/delete-navigation-menu-item-action-handler.service';
import { UpdateNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/update-navigation-menu-item-action-handler.service';
import { CreateCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/cron-trigger/services/create-cron-trigger-action-handler.service';
import { DeleteCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/cron-trigger/services/delete-cron-trigger-action-handler.service';
import { UpdateCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/cron-trigger/services/update-cron-trigger-action-handler.service';
@@ -142,6 +145,10 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
UpdateCommandMenuItemActionHandlerService,
DeleteCommandMenuItemActionHandlerService,
CreateNavigationMenuItemActionHandlerService,
UpdateNavigationMenuItemActionHandlerService,
DeleteNavigationMenuItemActionHandlerService,
CreatePageLayoutActionHandlerService,
UpdatePageLayoutActionHandlerService,
DeletePageLayoutActionHandlerService,
@@ -4,6 +4,7 @@ import { assertUnreachable } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type AllFlatEntityTypesByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name';
import { addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
import { addFlatNavigationMenuItemToMapsAndUpdateIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/add-flat-navigation-menu-item-to-maps-and-update-index.util';
type CreateAction<TMetadataName extends AllMetadataName> =
AllFlatEntityTypesByMetadataName[TMetadataName]['actions']['create'];
@@ -79,6 +80,15 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = <
return allFlatEntityMaps;
}
case 'navigationMenuItem': {
addFlatNavigationMenuItemToMapsAndUpdateIndex({
flatNavigationMenuItem: action.flatEntity,
flatNavigationMenuItemMaps:
allFlatEntityMaps.flatNavigationMenuItemMaps,
});
return allFlatEntityMaps;
}
default: {
assertUnreachable(action);
}
@@ -7,6 +7,7 @@ import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity
import { deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { deleteFlatNavigationMenuItemFromMapsAndIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/delete-flat-navigation-menu-item-from-maps-and-index.util';
type DeleteAction<TMetadataName extends AllMetadataName> =
AllFlatEntityTypesByMetadataName[TMetadataName]['actions']['delete'];
@@ -64,6 +65,21 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = <
return allFlatEntityMaps;
}
case 'navigationMenuItem': {
const flatNavigationMenuItemToDelete =
findFlatEntityByUniversalIdentifierOrThrow({
universalIdentifier: action.universalIdentifier,
flatEntityMaps: allFlatEntityMaps.flatNavigationMenuItemMaps,
});
deleteFlatNavigationMenuItemFromMapsAndIndex({
flatNavigationMenuItem: flatNavigationMenuItemToDelete,
flatNavigationMenuItemMaps:
allFlatEntityMaps.flatNavigationMenuItemMaps,
});
return allFlatEntityMaps;
}
default: {
assertUnreachable(action);
}
@@ -8,6 +8,7 @@ import { addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } f
import { deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { replaceFlatNavigationMenuItemInMapsAndUpdateIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/replace-flat-navigation-menu-item-in-maps-and-update-index.util';
import { replaceFlatEntityInFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/replace-flat-entity-in-flat-entity-maps-through-mutation-or-throw.util';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@@ -92,6 +93,27 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = <
return allFlatEntityMaps;
}
case 'navigationMenuItem': {
const fromFlatNavigationMenuItem =
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: action.entityId,
flatEntityMaps: allFlatEntityMaps.flatNavigationMenuItemMaps,
});
const toFlatNavigationMenuItem = {
...fromFlatNavigationMenuItem,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action),
};
replaceFlatNavigationMenuItemInMapsAndUpdateIndex({
fromFlatNavigationMenuItem,
toFlatNavigationMenuItem,
flatNavigationMenuItemMaps:
allFlatEntityMaps.flatNavigationMenuItemMaps,
});
return allFlatEntityMaps;
}
default: {
assertUnreachable(action);
}