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:
+8
-2
@@ -21,11 +21,17 @@ export const useNavigationMenuItemsByFolder = () => {
|
||||
const coreViews = useRecoilValue(coreViewsState);
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
|
||||
const { navigationMenuItems, workspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
|
||||
const { folders, itemsByFolderId } = navigationMenuItems.reduce<{
|
||||
const allNavigationMenuItems = [
|
||||
...navigationMenuItems,
|
||||
...workspaceNavigationMenuItems,
|
||||
];
|
||||
|
||||
const { folders, itemsByFolderId } = allNavigationMenuItems.reduce<{
|
||||
folders: Array<{ id: string; name: string }>;
|
||||
itemsByFolderId: Map<string, NavigationMenuItem[]>;
|
||||
}>(
|
||||
|
||||
+704
@@ -0,0 +1,704 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, IsNull, Not, Or, Repository } from 'typeorm';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.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';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import { FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
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';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_NAVIGATION_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-navigation-menu-item.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { FavoriteFolderWorkspaceEntity } from 'src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity';
|
||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
const WORKSPACE_LEVEL = 'workspace-level';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-17:migrate-favorites-to-navigation-menu-items',
|
||||
description: 'Migrate favorites and favoriteFolders to navigationMenuItems',
|
||||
})
|
||||
export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(
|
||||
MigrateFavoritesToNavigationMenuItemsCommand.name,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Starting migration of favorites to navigation menu items for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const isNavigationMenuItemEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isNavigationMenuItemEnabled) {
|
||||
this.logger.log(
|
||||
`Navigation menu item feature flag is already enabled for workspace ${workspaceId}. Skipping migration.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would migrate favorites to navigation menu items for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps,
|
||||
flatFieldMetadataMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatViewMaps',
|
||||
'flatNavigationMenuItemMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
]);
|
||||
|
||||
const {
|
||||
folderIdMapping,
|
||||
flatNavigationMenuItemsToCreate: folderNavItemsToCreate,
|
||||
} = await this.migrateFavoriteFolders({
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
existingFlatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
const { flatNavigationMenuItemsToCreate: favoriteNavItemsToCreate } =
|
||||
await this.migrateFavorites({
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
folderIdMapping,
|
||||
existingFlatNavigationMenuItemMaps,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const allFlatNavigationMenuItemsToCreate = [
|
||||
...folderNavItemsToCreate,
|
||||
...favoriteNavItemsToCreate,
|
||||
];
|
||||
|
||||
if (allFlatNavigationMenuItemsToCreate.length > 0) {
|
||||
const { flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatNavigationMenuItemMaps',
|
||||
]);
|
||||
|
||||
const twentyStandardItems = allFlatNavigationMenuItemsToCreate.filter(
|
||||
(item) => item.applicationId === twentyStandardFlatApplication.id,
|
||||
);
|
||||
const workspaceCustomItems = allFlatNavigationMenuItemsToCreate.filter(
|
||||
(item) => item.applicationId === workspaceCustomFlatApplication.id,
|
||||
);
|
||||
|
||||
if (workspaceCustomItems.length > 0) {
|
||||
await this.createNavigationMenuItems({
|
||||
workspaceId,
|
||||
flatNavigationMenuItemsToCreate: workspaceCustomItems,
|
||||
existingFlatNavigationMenuItemMaps,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
if (twentyStandardItems.length > 0) {
|
||||
await this.createNavigationMenuItems({
|
||||
workspaceId,
|
||||
flatNavigationMenuItemsToCreate: twentyStandardItems,
|
||||
existingFlatNavigationMenuItemMaps,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.featureFlagService.upsertWorkspaceFeatureFlag({
|
||||
workspaceId,
|
||||
featureFlag: FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Successfully migrated favorites to navigation menu items for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async migrateFavoriteFolders({
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
existingFlatNavigationMenuItemMaps,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
existingFlatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
}): Promise<{
|
||||
folderIdMapping: Map<string, Map<string, string>>;
|
||||
flatNavigationMenuItemsToCreate: FlatNavigationMenuItem[];
|
||||
}> {
|
||||
const existingFlatNavigationMenuItemIds = Object.keys(
|
||||
existingFlatNavigationMenuItemMaps.universalIdentifierById,
|
||||
);
|
||||
|
||||
const favoriteFolderRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<FavoriteFolderWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'favoriteFolder',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const favoriteFolders = await favoriteFolderRepository.find({
|
||||
where: {
|
||||
deletedAt: IsNull(),
|
||||
id: Not(In(existingFlatNavigationMenuItemIds)),
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
relations: {
|
||||
favorites: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (favoriteFolders.length === 0) {
|
||||
return {
|
||||
folderIdMapping: new Map(),
|
||||
flatNavigationMenuItemsToCreate: [],
|
||||
};
|
||||
}
|
||||
|
||||
const folderIdMapping = new Map<string, Map<string, string>>(); // favoriteFolderId -> workspaceMemberId or workspace-level -> navigationMenuItemId
|
||||
const flatNavigationMenuItemsToCreate: FlatNavigationMenuItem[] = [];
|
||||
|
||||
for (const favoriteFolder of favoriteFolders) {
|
||||
const workspaceMemberIdToNavigationMenuItemIdMap = new Map<
|
||||
string,
|
||||
string
|
||||
>();
|
||||
|
||||
const workspaceMemberIdsWithFavoritesInFolder = new Set(
|
||||
favoriteFolder.favorites
|
||||
.map((favorite) => favorite.forWorkspaceMemberId)
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
const hasWorkspaceScopedFavoritesInFolder = favoriteFolder.favorites.some(
|
||||
(favorite) => !isDefined(favorite.forWorkspaceMemberId),
|
||||
);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const [index, workspaceMemberId] of Array.from(
|
||||
workspaceMemberIdsWithFavoritesInFolder,
|
||||
).entries()) {
|
||||
const navigationMenuItemId = index === 0 ? favoriteFolder.id : uuidv4();
|
||||
|
||||
workspaceMemberIdToNavigationMenuItemIdMap.set(
|
||||
workspaceMemberId,
|
||||
navigationMenuItemId,
|
||||
);
|
||||
|
||||
const userWorkspaceId =
|
||||
await this.getUserWorkspaceIdFromWorkspaceMemberId(
|
||||
workspaceMemberId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const folderToCreate = {
|
||||
id: navigationMenuItemId,
|
||||
universalIdentifier: navigationMenuItemId,
|
||||
userWorkspaceId,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
viewId: null,
|
||||
folderId: null,
|
||||
name: favoriteFolder.name,
|
||||
position: favoriteFolder.position,
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
flatNavigationMenuItemsToCreate.push(folderToCreate);
|
||||
}
|
||||
|
||||
if (hasWorkspaceScopedFavoritesInFolder) {
|
||||
const workspaceLevelNavigationMenuItemId = uuidv4();
|
||||
|
||||
workspaceMemberIdToNavigationMenuItemIdMap.set(
|
||||
WORKSPACE_LEVEL,
|
||||
workspaceLevelNavigationMenuItemId,
|
||||
);
|
||||
|
||||
const workspaceLevelFolderToCreate = {
|
||||
id: workspaceLevelNavigationMenuItemId,
|
||||
universalIdentifier: workspaceLevelNavigationMenuItemId,
|
||||
userWorkspaceId: null,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
viewId: null,
|
||||
folderId: null,
|
||||
name: favoriteFolder.name,
|
||||
position: favoriteFolder.position,
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
flatNavigationMenuItemsToCreate.push(workspaceLevelFolderToCreate);
|
||||
}
|
||||
|
||||
folderIdMapping.set(
|
||||
favoriteFolder.id,
|
||||
workspaceMemberIdToNavigationMenuItemIdMap,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
folderIdMapping,
|
||||
flatNavigationMenuItemsToCreate,
|
||||
};
|
||||
}
|
||||
|
||||
private async migrateFavorites({
|
||||
workspaceId,
|
||||
twentyStandardApplicationId,
|
||||
workspaceCustomApplicationId,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
folderIdMapping,
|
||||
existingFlatNavigationMenuItemMaps,
|
||||
flatFieldMetadataMaps,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
twentyStandardApplicationId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatViewMaps: FlatEntityMaps<FlatView>;
|
||||
folderIdMapping: Map<string, Map<string, string>>;
|
||||
existingFlatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
}): Promise<{
|
||||
migratedFavoriteIds: string[];
|
||||
flatNavigationMenuItemsToCreate: FlatNavigationMenuItem[];
|
||||
}> {
|
||||
const favoriteRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<FavoriteWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'favorite',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const existingFlatNavigationMenuItemIds = Object.keys(
|
||||
existingFlatNavigationMenuItemMaps.universalIdentifierById,
|
||||
);
|
||||
|
||||
const viewIdsFromExistingNavigationMenuItems = Object.values(
|
||||
existingFlatNavigationMenuItemMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.map((item): string | null => item.viewId)
|
||||
.filter(isDefined);
|
||||
|
||||
const favorites = await favoriteRepository.find({
|
||||
where: {
|
||||
deletedAt: IsNull(),
|
||||
id: Not(In(existingFlatNavigationMenuItemIds)),
|
||||
viewId: Or(IsNull(), Not(In(viewIdsFromExistingNavigationMenuItems))),
|
||||
},
|
||||
order: { position: 'ASC' },
|
||||
});
|
||||
|
||||
if (favorites.length === 0) {
|
||||
return { migratedFavoriteIds: [], flatNavigationMenuItemsToCreate: [] };
|
||||
}
|
||||
|
||||
const favoriteObjectMetadata = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: STANDARD_OBJECTS.favorite.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(favoriteObjectMetadata)) {
|
||||
throw new Error('Favorite object metadata not found');
|
||||
}
|
||||
|
||||
const favoriteRelationFields =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityIds: favoriteObjectMetadata.fieldIds,
|
||||
}).filter(
|
||||
(field): field is FlatFieldMetadata<FieldMetadataType.RELATION> =>
|
||||
isFlatFieldMetadataOfType(field, FieldMetadataType.RELATION),
|
||||
);
|
||||
|
||||
const favoriteRelationFieldMap = new Map<
|
||||
string,
|
||||
{ name: string; relationTargetUniversalIdentifier: string }
|
||||
>();
|
||||
|
||||
for (const field of favoriteRelationFields) {
|
||||
if (
|
||||
field.name !== 'forWorkspaceMember' &&
|
||||
field.name !== 'favoriteFolder' &&
|
||||
isDefined(field.relationTargetObjectMetadataId)
|
||||
) {
|
||||
const targetObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
flatEntityId: field.relationTargetObjectMetadataId,
|
||||
});
|
||||
|
||||
if (isDefined(targetObjectMetadata)) {
|
||||
favoriteRelationFieldMap.set(field.name, {
|
||||
name: field.name,
|
||||
relationTargetUniversalIdentifier:
|
||||
targetObjectMetadata.universalIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const flatNavigationMenuItemsToCreate: FlatNavigationMenuItem[] = [];
|
||||
const migratedFavoriteIds: string[] = [];
|
||||
|
||||
for (const favorite of favorites) {
|
||||
const userWorkspaceId = !isDefined(favorite.forWorkspaceMemberId)
|
||||
? null
|
||||
: await this.getUserWorkspaceIdFromWorkspaceMemberId(
|
||||
favorite.forWorkspaceMemberId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(favorite.forWorkspaceMemberId) &&
|
||||
!isDefined(userWorkspaceId)
|
||||
) {
|
||||
this.logger.warn(
|
||||
`Skipping favorite ${favorite.id} - forWorkspaceMemberId ${favorite.forWorkspaceMemberId} did not resolve to a userWorkspaceId`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const folderId = favorite.favoriteFolderId
|
||||
? (folderIdMapping
|
||||
.get(favorite.favoriteFolderId)
|
||||
?.get(
|
||||
isDefined(favorite.forWorkspaceMemberId)
|
||||
? favorite.forWorkspaceMemberId
|
||||
: WORKSPACE_LEVEL,
|
||||
) ?? null)
|
||||
: null;
|
||||
|
||||
if (isDefined(favorite.viewId)) {
|
||||
const existingItem = Object.values(
|
||||
existingFlatNavigationMenuItemMaps.byUniversalIdentifier,
|
||||
).find(
|
||||
(item): item is FlatNavigationMenuItem =>
|
||||
isDefined(item) &&
|
||||
item.workspaceId === workspaceId &&
|
||||
item.userWorkspaceId === userWorkspaceId &&
|
||||
item.viewId === favorite.viewId &&
|
||||
item.folderId === folderId,
|
||||
);
|
||||
|
||||
if (isDefined(existingItem)) {
|
||||
this.logger.log(
|
||||
`Skipping favorite ${favorite.id} - navigation menu item already exists for view ${favorite.viewId}`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const { applicationId, universalIdentifier } =
|
||||
this.getApplicationIdAndUniversalIdentifierForViewFavorite({
|
||||
viewId: favorite.viewId,
|
||||
flatViewMaps,
|
||||
twentyStandardApplicationId,
|
||||
workspaceCustomApplicationId,
|
||||
});
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
flatNavigationMenuItemsToCreate.push({
|
||||
id: favorite.id,
|
||||
universalIdentifier,
|
||||
userWorkspaceId,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
viewId: favorite.viewId,
|
||||
folderId,
|
||||
name: null,
|
||||
position: favorite.position,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
migratedFavoriteIds.push(favorite.id);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const { targetRecordId, targetObjectMetadataId } =
|
||||
this.getTargetRecordFromFavorite(
|
||||
favorite,
|
||||
flatObjectMetadataMaps,
|
||||
favoriteRelationFieldMap,
|
||||
);
|
||||
|
||||
if (!isDefined(targetRecordId) || !isDefined(targetObjectMetadataId)) {
|
||||
this.logger.warn(
|
||||
`Skipping favorite ${favorite.id} - no valid target record found`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
flatNavigationMenuItemsToCreate.push({
|
||||
id: favorite.id,
|
||||
universalIdentifier: favorite.id,
|
||||
userWorkspaceId,
|
||||
targetRecordId,
|
||||
targetObjectMetadataId,
|
||||
viewId: null,
|
||||
folderId,
|
||||
name: null,
|
||||
position: favorite.position,
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
migratedFavoriteIds.push(favorite.id);
|
||||
}
|
||||
|
||||
return {
|
||||
migratedFavoriteIds,
|
||||
flatNavigationMenuItemsToCreate,
|
||||
};
|
||||
}
|
||||
|
||||
private getApplicationIdAndUniversalIdentifierForViewFavorite({
|
||||
viewId,
|
||||
flatViewMaps,
|
||||
twentyStandardApplicationId,
|
||||
workspaceCustomApplicationId,
|
||||
}: {
|
||||
viewId: string;
|
||||
flatViewMaps: FlatEntityMaps<FlatView>;
|
||||
twentyStandardApplicationId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
}): { applicationId: string; universalIdentifier: string } {
|
||||
const flatView = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatViewMaps,
|
||||
flatEntityId: viewId,
|
||||
});
|
||||
|
||||
if (!isDefined(flatView)) {
|
||||
throw new Error(`View ${viewId} not found`);
|
||||
}
|
||||
|
||||
const matchingStandardItem = Object.values(
|
||||
STANDARD_NAVIGATION_MENU_ITEMS,
|
||||
).find(
|
||||
(item) => item.viewUniversalIdentifier === flatView.universalIdentifier,
|
||||
);
|
||||
|
||||
return !isDefined(matchingStandardItem)
|
||||
? {
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
universalIdentifier: uuidv4(),
|
||||
}
|
||||
: {
|
||||
applicationId: twentyStandardApplicationId,
|
||||
universalIdentifier: matchingStandardItem.universalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
private async getUserWorkspaceIdFromWorkspaceMemberId(
|
||||
workspaceMemberId: string | null | undefined,
|
||||
workspaceId: string,
|
||||
): Promise<string | null> {
|
||||
if (!isDefined(workspaceMemberId)) {
|
||||
return null;
|
||||
}
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
try {
|
||||
const workspaceMember =
|
||||
await this.twentyORMGlobalManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
return await workspaceMemberRepository.findOne({
|
||||
where: { id: workspaceMemberId },
|
||||
});
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
|
||||
if (!isDefined(workspaceMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userWorkspace =
|
||||
await this.userWorkspaceService.getUserWorkspaceForUserOrThrow({
|
||||
userId: workspaceMember.userId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return userWorkspace.id;
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to get userWorkspaceId for workspaceMemberId ${workspaceMemberId}: ${error}`,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private getTargetRecordFromFavorite(
|
||||
favorite: FavoriteWorkspaceEntity,
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
|
||||
favoriteRelationFieldMap: Map<
|
||||
string,
|
||||
{ name: string; relationTargetUniversalIdentifier: string }
|
||||
>,
|
||||
): {
|
||||
targetRecordId: string | null;
|
||||
targetObjectMetadataId: string | null;
|
||||
} {
|
||||
for (const [
|
||||
fieldName,
|
||||
{ relationTargetUniversalIdentifier },
|
||||
] of favoriteRelationFieldMap.entries()) {
|
||||
const fieldIdName = `${fieldName}Id`;
|
||||
const recordId = (
|
||||
favorite as unknown as Record<string, string | undefined>
|
||||
)[fieldIdName];
|
||||
|
||||
if (!isDefined(recordId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetObjectMetadata = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: relationTargetUniversalIdentifier,
|
||||
});
|
||||
|
||||
if (isDefined(targetObjectMetadata)) {
|
||||
return {
|
||||
targetRecordId: recordId,
|
||||
targetObjectMetadataId: targetObjectMetadata.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { targetRecordId: null, targetObjectMetadataId: null };
|
||||
}
|
||||
|
||||
private async createNavigationMenuItems({
|
||||
workspaceId,
|
||||
flatNavigationMenuItemsToCreate,
|
||||
existingFlatNavigationMenuItemMaps: _existingFlatNavigationMenuItemMaps,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatNavigationMenuItemsToCreate: FlatNavigationMenuItem[];
|
||||
existingFlatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<void> {
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
navigationMenuItem: {
|
||||
flatEntityToCreate: flatNavigationMenuItemsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
applicationUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new Error(
|
||||
`Failed to create navigation menu items: ${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -7,6 +7,7 @@ import { FixMorphRelationFieldNamesCommand } from 'src/database/commands/upgrade
|
||||
import { IdentifyWebhookMetadataCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command';
|
||||
import { MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MigrateAttachmentToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command';
|
||||
import { MigrateFavoritesToNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-favorites-to-navigation-menu-items.command';
|
||||
import { MigrateNoteTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command';
|
||||
import { MigrateSendEmailRecipientsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-send-email-recipients.command';
|
||||
import { MigrateTaskTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command';
|
||||
@@ -19,6 +20,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
@@ -32,6 +34,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
|
||||
import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||
import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/note-target.workspace-entity';
|
||||
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
||||
@@ -59,6 +62,8 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas
|
||||
FieldMetadataModule,
|
||||
ObjectMetadataModule,
|
||||
ApplicationModule,
|
||||
UserWorkspaceModule,
|
||||
WorkspaceMigrationModule,
|
||||
LogicFunctionModule,
|
||||
RecordPositionModule,
|
||||
GlobalWorkspaceDataSourceModule,
|
||||
@@ -66,6 +71,7 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas
|
||||
providers: [
|
||||
FixMorphRelationFieldNamesCommand,
|
||||
MigrateAttachmentToMorphRelationsCommand,
|
||||
MigrateFavoritesToNavigationMenuItemsCommand,
|
||||
MigrateNoteTargetToMorphRelationsCommand,
|
||||
MigrateTaskTargetToMorphRelationsCommand,
|
||||
IdentifyWebhookMetadataCommand,
|
||||
@@ -80,6 +86,7 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas
|
||||
exports: [
|
||||
FixMorphRelationFieldNamesCommand,
|
||||
MigrateAttachmentToMorphRelationsCommand,
|
||||
MigrateFavoritesToNavigationMenuItemsCommand,
|
||||
MigrateNoteTargetToMorphRelationsCommand,
|
||||
MigrateTaskTargetToMorphRelationsCommand,
|
||||
IdentifyWebhookMetadataCommand,
|
||||
|
||||
+3
@@ -15,6 +15,7 @@ import { FixMorphRelationFieldNamesCommand } from 'src/database/commands/upgrade
|
||||
import { IdentifyWebhookMetadataCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command';
|
||||
import { MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MigrateAttachmentToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command';
|
||||
import { MigrateFavoritesToNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-favorites-to-navigation-menu-items.command';
|
||||
import { MigrateNoteTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command';
|
||||
import { MigrateTaskTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command';
|
||||
import { MigrateWorkflowCodeStepsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command';
|
||||
@@ -42,6 +43,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly backfillApplicationPackageFilesCommand: BackfillApplicationPackageFilesCommand,
|
||||
protected readonly deleteFileRecordsCommand: DeleteFileRecordsCommand,
|
||||
protected readonly migrateAttachmentToMorphRelationsCommand: MigrateAttachmentToMorphRelationsCommand,
|
||||
protected readonly migrateFavoritesToNavigationMenuItemsCommand: MigrateFavoritesToNavigationMenuItemsCommand,
|
||||
protected readonly migrateNoteTargetToMorphRelationsCommand: MigrateNoteTargetToMorphRelationsCommand,
|
||||
protected readonly migrateTaskTargetToMorphRelationsCommand: MigrateTaskTargetToMorphRelationsCommand,
|
||||
protected readonly identifyWebhookMetadataCommand: IdentifyWebhookMetadataCommand,
|
||||
@@ -62,6 +64,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
|
||||
const commands_1170: VersionCommands = [
|
||||
this.migrateAttachmentToMorphRelationsCommand,
|
||||
this.migrateFavoritesToNavigationMenuItemsCommand,
|
||||
this.migrateNoteTargetToMorphRelationsCommand,
|
||||
this.migrateTaskTargetToMorphRelationsCommand,
|
||||
this.identifyWebhookMetadataCommand,
|
||||
|
||||
+77
-4
@@ -4,9 +4,12 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils';
|
||||
import { FindManyOptions, FindOneOptions, Repository } from 'typeorm';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
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';
|
||||
@@ -16,6 +19,7 @@ import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/
|
||||
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';
|
||||
import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util';
|
||||
import { fromDeleteObjectInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util';
|
||||
@@ -51,6 +55,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {
|
||||
super(objectMetadataRepository);
|
||||
}
|
||||
@@ -409,6 +414,18 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatObjectMetadataToCreate.labelIdentifierFieldMetadataId,
|
||||
});
|
||||
|
||||
const isNavigationMenuItemEnabled =
|
||||
existingFeatureFlagsMap[FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED] ??
|
||||
false;
|
||||
|
||||
const flatNavigationMenuItemToCreate = isNavigationMenuItemEnabled
|
||||
? await this.computeFlatNavigationMenuItemToCreate({
|
||||
view: flatDefaultViewToCreate,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
})
|
||||
: null;
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
@@ -441,6 +458,15 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
...(isDefined(flatNavigationMenuItemToCreate)
|
||||
? {
|
||||
navigationMenuItem: {
|
||||
flatEntityToCreate: [flatNavigationMenuItemToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
@@ -476,10 +502,12 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
);
|
||||
}
|
||||
|
||||
await this.createWorkspaceFavoriteForNewObjectDefaultView({
|
||||
view: flatDefaultViewToCreate,
|
||||
workspaceId,
|
||||
});
|
||||
if (!isNavigationMenuItemEnabled) {
|
||||
await this.createWorkspaceFavoriteForNewObjectDefaultView({
|
||||
view: flatDefaultViewToCreate,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
return createdFlatObjectMetadata;
|
||||
}
|
||||
@@ -551,6 +579,51 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
return defaultViewFields;
|
||||
}
|
||||
|
||||
private async computeFlatNavigationMenuItemToCreate({
|
||||
view,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
}: {
|
||||
view: FlatView;
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
}): Promise<FlatNavigationMenuItem> {
|
||||
const { flatNavigationMenuItemMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatNavigationMenuItemMaps',
|
||||
]);
|
||||
|
||||
const workspaceLevelItems = Object.values(
|
||||
flatNavigationMenuItemMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(item): item is FlatNavigationMenuItem =>
|
||||
isDefined(item) && !isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const nextPosition =
|
||||
workspaceLevelItems.length > 0
|
||||
? Math.max(...workspaceLevelItems.map((item) => item.position)) + 1
|
||||
: 0;
|
||||
|
||||
const newId = uuidv4();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return {
|
||||
id: newId,
|
||||
universalIdentifier: newId,
|
||||
userWorkspaceId: null,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
viewId: view.id,
|
||||
folderId: null,
|
||||
name: null,
|
||||
position: nextPosition,
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
private async createWorkspaceFavoriteForNewObjectDefaultView({
|
||||
view,
|
||||
workspaceId,
|
||||
|
||||
+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