Migrate favorites to navigation menu items (1.17 upgrade) (#17477)
**What this PR does:** - Migrates existing `Favorite` and `FavoriteFolder` entities to the new `NavigationMenuItem` structure - Preserves user-level vs workspace-level ownership - Preserves folder structure, positions, and relationships - Handles both view-based favorites (linked to views) and record-based favorites (linked to records) - Soft-deletes original favorites and folders after successful migration - Enables the `IS_NAVIGATION_MENU_ITEM_ENABLED` feature flag post-migration - Skips migration if the feature flag is already enabled - Idempotent: checks for existing navigation menu items to prevent duplicates **Migration flow:** 1. Migrate favorite folders first (creates folder mapping) 2. Migrate favorites 3. Soft-delete migrated favorites and folders 4. Enable feature flag **What's next:** - After all workspaces are migrated and the navigation menu item feature is fully rolled out, we can: - Remove the old `Favorite` and `FavoriteFolder` entities and related code - Remove the feature flag check and make navigation menu items the default - Clean up any deprecated favorites-related code paths in the frontend --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
This commit is contained in:
+1
-78
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@@ -10,7 +10,6 @@ import { MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/type
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getSubFlatEntityMapsByApplicationIdOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-by-application-id-or-throw.util';
|
||||
import { fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-create-navigation-menu-item-input-to-flat-navigation-menu-item-to-create.util';
|
||||
import { FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
@@ -60,82 +59,6 @@ export class TwentyStandardApplicationService {
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
private async createManyNavigationMenuItem({
|
||||
flatViews,
|
||||
workspaceId,
|
||||
}: {
|
||||
flatViews: FlatView[];
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const existingWorkspaceItems = Object.values(
|
||||
existingFlatNavigationMenuItemMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(item) =>
|
||||
isDefined(item) &&
|
||||
item.workspaceId === workspaceId &&
|
||||
item.userWorkspaceId === null,
|
||||
);
|
||||
|
||||
const maxPosition = existingWorkspaceItems.reduce(
|
||||
(max, item) => Math.max(max, item?.position ?? 0),
|
||||
0,
|
||||
);
|
||||
|
||||
const flatNavigationMenuItemsToCreate = flatViews.map((flatView, index) => {
|
||||
return fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate({
|
||||
createNavigationMenuItemInput: {
|
||||
viewId: flatView.id,
|
||||
userWorkspaceId: undefined,
|
||||
position: maxPosition + index + 1,
|
||||
},
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomFlatApplication.id,
|
||||
flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps,
|
||||
});
|
||||
});
|
||||
|
||||
if (flatNavigationMenuItemsToCreate.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
navigationMenuItem: {
|
||||
flatEntityToCreate: flatNavigationMenuItemsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while creating workspace navigation menu items',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async synchronizeTwentyStandardApplicationOrThrow({
|
||||
workspaceId,
|
||||
}: {
|
||||
|
||||
Reference in New Issue
Block a user