Integrate NavigationMenuItem with feature flag support (#17268)
## Implement Navigation Menu Items Frontend Implements the frontend for navigation menu items, the new system replacing favorites. ### Changes - Added GraphQL fragments and queries for navigation menu items - Added hooks for managing navigation menu items (create, update, delete, sorting, filtering) - Updated components to use navigation menu items instead of favorites - Added test coverage for utility functions ### Migration Note The favorites and navigation menu item modules currently exist in parallel. The favorites code will be removed once all data has been migrated to navigation menu items. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Replaces Favorites with feature-flagged `NavigationMenuItem` across frontend and backend, while keeping Favorites as fallback until migration completes. > > - UI: new `navigation-menu-item` components (folders, orphan items, drag provider/droppable, icons, skeleton), dispatcher components to switch from Favorites, and updated “Add to favorites” action to create `NavigationMenuItem` when `IS_NAVIGATION_MENU_ITEM_ENABLED` > - DnD: shared `validateAndExtractFolderId` and droppable id utils moved to `ui/layout/draggable-list`; favorites DnD updated to use shared utils > - GraphQL (client): add fragments, queries, mutations, hooks (create/update/delete/find), and generated types; added `RecordIdentifier` and `targetRecordIdentifier` on `NavigationMenuItem` > - Prefetch: new prefetch state/effect for navigation menu items; skip favorites prefetch when flag enabled > - Backend: add DTOs (`NavigationMenuItem`, `RecordIdentifier`), resolver `targetRecordIdentifier` field, service logic to fetch record identifiers with permission-aware access and image signing, `getRecordImageIdentifier` util, entity relation to `view`, and migration adding FK on `viewId` > - Feature flags & seeding: add `IS_NAVIGATION_MENU_ITEM_ENABLED` to enums, dev seeder enables it; standard app seeds workspace navigation menu items instead of favorites when flag on > - Tests: add unit tests for sorting/labels/folder id and related utils > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c99746f08b9f84fc8cec4fcc3a7d7afb8ea92db7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Aman Raj <92664006+araj00@users.noreply.github.com> Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
+1
@@ -14,5 +14,6 @@ export enum FeatureFlagKey {
|
||||
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
|
||||
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
|
||||
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
|
||||
IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED',
|
||||
IS_FILES_FIELD_ENABLED = 'IS_FILES_FIELD_ENABLED',
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/cale
|
||||
import { CalendarModule } from 'src/modules/calendar/calendar.module';
|
||||
import { AutoCompaniesAndContactsCreationJobModule } from 'src/modules/contact-creation-manager/jobs/auto-companies-and-contacts-creation-job.module';
|
||||
import { FavoriteModule } from 'src/modules/favorite/favorite.module';
|
||||
import { NavigationMenuItemModule } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module';
|
||||
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
||||
import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module';
|
||||
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
|
||||
@@ -62,6 +63,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||
WebhookJobModule,
|
||||
WorkflowModule,
|
||||
FavoriteModule,
|
||||
NavigationMenuItemModule,
|
||||
WorkspaceCleanerModule,
|
||||
SubscriptionsModule,
|
||||
AuditJobModule,
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { getLogoUrlFromDomainName, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type 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 { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
type GetRecordImageIdentifierOptions = {
|
||||
record: Record<string, unknown>;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
signUrl?: (url: string) => string | null;
|
||||
};
|
||||
|
||||
export const getRecordImageIdentifier = ({
|
||||
record,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
signUrl,
|
||||
}: GetRecordImageIdentifierOptions): string | null => {
|
||||
if (flatObjectMetadata.nameSingular === 'company') {
|
||||
const domainNameObj = record.domainName as
|
||||
| { primaryLinkUrl?: string }
|
||||
| undefined;
|
||||
const domainNamePrimaryLinkUrl = domainNameObj?.primaryLinkUrl;
|
||||
|
||||
return domainNamePrimaryLinkUrl
|
||||
? getLogoUrlFromDomainName(domainNamePrimaryLinkUrl) || null
|
||||
: null;
|
||||
}
|
||||
|
||||
if (!isDefined(flatObjectMetadata.imageIdentifierFieldMetadataId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageIdentifierField = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityId: flatObjectMetadata.imageIdentifierFieldMetadataId,
|
||||
});
|
||||
|
||||
if (!isDefined(imageIdentifierField)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageValue = record[imageIdentifierField.name];
|
||||
|
||||
if (!isDefined(imageValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawImageValue = String(imageValue);
|
||||
|
||||
if (!isNonEmptyString(rawImageValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
signUrl &&
|
||||
(flatObjectMetadata.nameSingular === 'person' ||
|
||||
flatObjectMetadata.nameSingular === 'workspaceMember')
|
||||
) {
|
||||
return signUrl(rawImageValue);
|
||||
}
|
||||
|
||||
return rawImageValue;
|
||||
};
|
||||
+5
@@ -106,6 +106,11 @@ export const ALL_METADATA_RELATIONS = {
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'folderId',
|
||||
},
|
||||
view: {
|
||||
metadataName: 'view',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'viewId',
|
||||
},
|
||||
},
|
||||
oneToMany: {},
|
||||
},
|
||||
|
||||
+1
@@ -42,6 +42,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
[
|
||||
"objectMetadata",
|
||||
"navigationMenuItem",
|
||||
"view",
|
||||
]
|
||||
`;
|
||||
|
||||
|
||||
+1
-1
@@ -3,8 +3,8 @@
|
||||
exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with children first (most manyToOne relations first) 1`] = `
|
||||
[
|
||||
"rowLevelPermissionPredicate",
|
||||
"viewFilter",
|
||||
"navigationMenuItem",
|
||||
"viewFilter",
|
||||
"pageLayoutWidget",
|
||||
"viewField",
|
||||
"commandMenuItem",
|
||||
|
||||
+5
@@ -10,6 +10,8 @@ import {
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
import { RecordIdentifierDTO } from './record-identifier.dto';
|
||||
|
||||
@ObjectType('NavigationMenuItem')
|
||||
export class NavigationMenuItemDTO {
|
||||
@IsUUID()
|
||||
@@ -64,4 +66,7 @@ export class NavigationMenuItemDTO {
|
||||
@IsDateString()
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
|
||||
@Field(() => RecordIdentifierDTO, { nullable: true })
|
||||
targetRecordIdentifier?: RecordIdentifierDTO | null;
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('RecordIdentifier')
|
||||
export class RecordIdentifierDTO {
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@Field(() => String)
|
||||
labelIdentifier: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field(() => String, { nullable: true })
|
||||
imageIdentifier?: string | null;
|
||||
}
|
||||
+8
@@ -13,6 +13,7 @@ import {
|
||||
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
@Entity({ name: 'navigationMenuItem', schema: 'core' })
|
||||
@@ -63,6 +64,13 @@ export class NavigationMenuItemEntity
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
viewId: string | null;
|
||||
|
||||
@ManyToOne(() => ViewEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
nullable: true,
|
||||
})
|
||||
@JoinColumn({ name: 'viewId' })
|
||||
view: Relation<ViewEntity> | null;
|
||||
|
||||
@ManyToOne(() => ObjectMetadataEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
nullable: true,
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { Scope } from '@nestjs/common';
|
||||
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { NavigationMenuItemDeletionService } from 'src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-deletion.service';
|
||||
|
||||
export type NavigationMenuItemDeletionJobData = {
|
||||
workspaceId: string;
|
||||
deletedRecordIds: string[];
|
||||
};
|
||||
|
||||
@Processor({
|
||||
queueName: MessageQueue.deleteCascadeQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class NavigationMenuItemDeletionJob {
|
||||
constructor(
|
||||
private readonly navigationMenuItemDeletionService: NavigationMenuItemDeletionService,
|
||||
) {}
|
||||
|
||||
@Process(NavigationMenuItemDeletionJob.name)
|
||||
async handle(data: NavigationMenuItemDeletionJobData): Promise<void> {
|
||||
await this.navigationMenuItemDeletionService.deleteNavigationMenuItemsForDeletedRecords(
|
||||
data.deletedRecordIds,
|
||||
data.workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ObjectRecordDeleteEvent } from 'twenty-shared/database-events';
|
||||
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import {
|
||||
NavigationMenuItemDeletionJob,
|
||||
type NavigationMenuItemDeletionJobData,
|
||||
} from 'src/engine/metadata-modules/navigation-menu-item/jobs/navigation-menu-item-deletion.job';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
|
||||
@Injectable()
|
||||
export class NavigationMenuItemDeletionListener {
|
||||
constructor(
|
||||
@InjectMessageQueue(MessageQueue.deleteCascadeQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
) {}
|
||||
|
||||
@OnDatabaseBatchEvent('*', DatabaseEventAction.DELETED)
|
||||
async handleDeletedEvent(
|
||||
payload: WorkspaceEventBatch<ObjectRecordDeleteEvent>,
|
||||
) {
|
||||
const deletedRecordIds = payload.events.map(({ recordId }) => recordId);
|
||||
|
||||
if (deletedRecordIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.messageQueueService.add<NavigationMenuItemDeletionJobData>(
|
||||
NavigationMenuItemDeletionJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
deletedRecordIds,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+12
@@ -1,13 +1,19 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { FlatNavigationMenuItemModule } from 'src/engine/metadata-modules/flat-navigation-menu-item/flat-navigation-menu-item.module';
|
||||
import { NavigationMenuItemGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/navigation-menu-item/interceptors/navigation-menu-item-graphql-api-exception.interceptor';
|
||||
import { NavigationMenuItemDeletionJob } from 'src/engine/metadata-modules/navigation-menu-item/jobs/navigation-menu-item-deletion.job';
|
||||
import { NavigationMenuItemDeletionListener } from 'src/engine/metadata-modules/navigation-menu-item/listeners/navigation-menu-item-deletion.listener';
|
||||
import { NavigationMenuItemResolver } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver';
|
||||
import { NavigationMenuItemService } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service';
|
||||
import { NavigationMenuItemAccessService } from 'src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-access.service';
|
||||
import { NavigationMenuItemDeletionService } from 'src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-deletion.service';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@@ -18,10 +24,16 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
ApplicationModule,
|
||||
FlatNavigationMenuItemModule,
|
||||
PermissionsModule,
|
||||
FileModule,
|
||||
UserRoleModule,
|
||||
ApiKeyModule,
|
||||
],
|
||||
providers: [
|
||||
NavigationMenuItemService,
|
||||
NavigationMenuItemAccessService,
|
||||
NavigationMenuItemDeletionService,
|
||||
NavigationMenuItemDeletionListener,
|
||||
NavigationMenuItemDeletionJob,
|
||||
NavigationMenuItemResolver,
|
||||
NavigationMenuItemGraphqlApiExceptionInterceptor,
|
||||
WorkspaceMigrationGraphqlApiExceptionInterceptor,
|
||||
|
||||
+35
-1
@@ -1,5 +1,17 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import {
|
||||
Args,
|
||||
Context,
|
||||
Mutation,
|
||||
Parent,
|
||||
Query,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
@@ -12,6 +24,7 @@ import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/create-navigation-menu-item.input';
|
||||
import { NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto';
|
||||
import { RecordIdentifierDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/record-identifier.dto';
|
||||
import { UpdateOneNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input';
|
||||
import { NavigationMenuItemGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/navigation-menu-item/interceptors/navigation-menu-item-graphql-api-exception.interceptor';
|
||||
import { NavigationMenuItemService } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service';
|
||||
@@ -105,4 +118,25 @@ export class NavigationMenuItemResolver {
|
||||
authApplicationId: context.req.application?.id,
|
||||
});
|
||||
}
|
||||
|
||||
@ResolveField(() => RecordIdentifierDTO, { nullable: true })
|
||||
async targetRecordIdentifier(
|
||||
@Parent() navigationMenuItem: NavigationMenuItemDTO,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@Context() context: { req: WorkspaceAuthContext },
|
||||
): Promise<RecordIdentifierDTO | null> {
|
||||
if (
|
||||
!isDefined(navigationMenuItem.targetRecordId) ||
|
||||
!isDefined(navigationMenuItem.targetObjectMetadataId)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.navigationMenuItemService.findTargetRecord({
|
||||
targetRecordId: navigationMenuItem.targetRecordId,
|
||||
targetObjectMetadataId: navigationMenuItem.targetObjectMetadataId,
|
||||
workspaceId: workspace.id,
|
||||
authContext: context.req,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+163
@@ -2,7 +2,13 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { getRecordDisplayName } from 'src/engine/core-modules/record-crud/utils/get-record-display-name.util';
|
||||
import { getRecordImageIdentifier } from 'src/engine/core-modules/record-crud/utils/get-record-image-identifier.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
@@ -12,12 +18,19 @@ import { fromFlatNavigationMenuItemToNavigationMenuItemDto } from 'src/engine/me
|
||||
import { fromUpdateNavigationMenuItemInputToFlatNavigationMenuItemToUpdateOrThrow } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-update-navigation-menu-item-input-to-flat-navigation-menu-item-to-update-or-throw.util';
|
||||
import { type CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/create-navigation-menu-item.input';
|
||||
import { type NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto';
|
||||
import { RecordIdentifierDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/record-identifier.dto';
|
||||
import { type UpdateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input';
|
||||
import {
|
||||
NavigationMenuItemException,
|
||||
NavigationMenuItemExceptionCode,
|
||||
} from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.exception';
|
||||
import { NavigationMenuItemAccessService } from 'src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-access.service';
|
||||
import { getMinimalSelectForRecordIdentifier } from 'src/engine/metadata-modules/navigation-menu-item/utils/get-minimal-select-for-record-identifier.util';
|
||||
import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@@ -28,6 +41,10 @@ export class NavigationMenuItemService {
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly navigationMenuItemAccessService: NavigationMenuItemAccessService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly fileService: FileService,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly apiKeyRoleService: ApiKeyRoleService,
|
||||
) {}
|
||||
|
||||
async findAll({
|
||||
@@ -334,4 +351,150 @@ export class NavigationMenuItemService {
|
||||
flatNavigationMenuItemToDelete,
|
||||
);
|
||||
}
|
||||
|
||||
private async getRoleId(
|
||||
authContext: WorkspaceAuthContext,
|
||||
workspaceId: string,
|
||||
): Promise<string | undefined> {
|
||||
if (isDefined(authContext.apiKey)) {
|
||||
return this.apiKeyRoleService.getRoleIdForApiKeyId(
|
||||
authContext.apiKey.id,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(authContext.application?.defaultServerlessFunctionRoleId)) {
|
||||
return authContext.application.defaultServerlessFunctionRoleId;
|
||||
}
|
||||
|
||||
if (isDefined(authContext.userWorkspaceId)) {
|
||||
try {
|
||||
return await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof PermissionsException) {
|
||||
return undefined;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async findTargetRecord({
|
||||
targetRecordId,
|
||||
targetObjectMetadataId,
|
||||
workspaceId,
|
||||
authContext,
|
||||
}: {
|
||||
targetRecordId: string;
|
||||
targetObjectMetadataId: string;
|
||||
workspaceId: string;
|
||||
authContext: WorkspaceAuthContext;
|
||||
}): Promise<RecordIdentifierDTO | null> {
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const objectMetadata = flatObjectMetadataMaps.byId[targetObjectMetadataId];
|
||||
|
||||
if (!isDefined(objectMetadata)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const roleId = await this.getRoleId(authContext, workspaceId);
|
||||
|
||||
if (!isDefined(roleId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rolePermissionConfig: RolePermissionConfig = {
|
||||
unionOf: [roleId],
|
||||
};
|
||||
|
||||
const minimalSelectColumns = getMinimalSelectForRecordIdentifier({
|
||||
flatObjectMetadata: objectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const record =
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const repository =
|
||||
await this.globalWorkspaceOrmManager.getRepository(
|
||||
workspaceId,
|
||||
objectMetadata.nameSingular,
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const alias = objectMetadata.nameSingular;
|
||||
const queryBuilder = repository.createQueryBuilder(alias);
|
||||
|
||||
queryBuilder.select([]);
|
||||
|
||||
for (const column of minimalSelectColumns) {
|
||||
queryBuilder.addSelect(`"${alias}"."${column}"`, column);
|
||||
}
|
||||
|
||||
const rawResult = await queryBuilder
|
||||
.where(`${alias}.id = :id`, { id: targetRecordId })
|
||||
.getRawOne();
|
||||
|
||||
if (!isDefined(rawResult)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formattedRecord = formatResult<Record<string, unknown>>(
|
||||
rawResult,
|
||||
objectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
);
|
||||
|
||||
return formattedRecord;
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(record)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelIdentifier = getRecordDisplayName(
|
||||
record,
|
||||
objectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
);
|
||||
|
||||
const imageIdentifier = getRecordImageIdentifier({
|
||||
record,
|
||||
flatObjectMetadata: objectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
signUrl: (url: string) =>
|
||||
this.fileService.signFileUrl({
|
||||
url,
|
||||
workspaceId,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
id: record.id as string,
|
||||
labelIdentifier,
|
||||
imageIdentifier,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof PermissionsException) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { fromDeleteNavigationMenuItemInputToFlatNavigationMenuItemOrThrow } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-delete-navigation-menu-item-input-to-flat-navigation-menu-item-or-throw.util';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@Injectable()
|
||||
export class NavigationMenuItemDeletionService {
|
||||
constructor(
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
) {}
|
||||
|
||||
async deleteNavigationMenuItemsForDeletedRecords(
|
||||
deletedRecordIds: string[],
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const { flatNavigationMenuItemMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const deletedRecordIdsSet = new Set(deletedRecordIds);
|
||||
|
||||
const navigationMenuItemsToDelete = Object.values(
|
||||
flatNavigationMenuItemMaps.byId,
|
||||
).filter(
|
||||
(item): item is NonNullable<typeof item> =>
|
||||
isDefined(item) &&
|
||||
isDefined(item.targetRecordId) &&
|
||||
!isDefined(item.viewId) &&
|
||||
deletedRecordIdsSet.has(item.targetRecordId),
|
||||
);
|
||||
|
||||
if (navigationMenuItemsToDelete.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const flatNavigationMenuItemsToDelete = navigationMenuItemsToDelete.map(
|
||||
(item) =>
|
||||
fromDeleteNavigationMenuItemInputToFlatNavigationMenuItemOrThrow({
|
||||
flatNavigationMenuItemMaps,
|
||||
navigationMenuItemId: item.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
navigationMenuItem: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: flatNavigationMenuItemsToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while deleting navigation menu items for deleted records',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { compositeTypeDefinitions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type 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 { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
const ID_FIELD = 'id' as const;
|
||||
|
||||
const COMPANY_AVATAR_COLUMN = 'domainNamePrimaryLinkUrl' as const;
|
||||
|
||||
export const getMinimalSelectForRecordIdentifier = ({
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
}: {
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
}): string[] => {
|
||||
const selectColumns: string[] = [ID_FIELD];
|
||||
|
||||
const labelField = isDefined(
|
||||
flatObjectMetadata.labelIdentifierFieldMetadataId,
|
||||
)
|
||||
? findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityId: flatObjectMetadata.labelIdentifierFieldMetadataId,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (isDefined(labelField)) {
|
||||
const labelCompositeType = isCompositeFieldMetadataType(labelField.type)
|
||||
? compositeTypeDefinitions.get(labelField.type)
|
||||
: undefined;
|
||||
|
||||
if (isDefined(labelCompositeType)) {
|
||||
for (const compositeProperty of labelCompositeType.properties) {
|
||||
selectColumns.push(
|
||||
computeCompositeColumnName(labelField.name, compositeProperty),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
selectColumns.push(labelField.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (flatObjectMetadata.nameSingular === 'company') {
|
||||
selectColumns.push(COMPANY_AVATAR_COLUMN);
|
||||
} else if (isDefined(flatObjectMetadata.imageIdentifierFieldMetadataId)) {
|
||||
const imageField = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityId: flatObjectMetadata.imageIdentifierFieldMetadataId,
|
||||
});
|
||||
|
||||
if (isDefined(imageField)) {
|
||||
const imageCompositeType = isCompositeFieldMetadataType(imageField.type)
|
||||
? compositeTypeDefinitions.get(imageField.type)
|
||||
: undefined;
|
||||
|
||||
if (isDefined(imageCompositeType)) {
|
||||
for (const compositeProperty of imageCompositeType.properties) {
|
||||
selectColumns.push(
|
||||
computeCompositeColumnName(imageField.name, compositeProperty),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
selectColumns.push(imageField.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selectColumns;
|
||||
};
|
||||
+1
@@ -223,6 +223,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_JUNCTION_RELATIONS_ENABLED: false,
|
||||
IS_SSE_DB_EVENTS_ENABLED: false,
|
||||
IS_COMMAND_MENU_ITEM_ENABLED: false,
|
||||
IS_NAVIGATION_MENU_ITEM_ENABLED: false,
|
||||
IS_FILES_FIELD_ENABLED: false,
|
||||
IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED: false,
|
||||
},
|
||||
|
||||
+5
@@ -81,6 +81,11 @@ export const seedFeatureFlags = async ({
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_FILES_FIELD_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { STANDARD_OBJECTS } from './standard-object.constant';
|
||||
|
||||
export const STANDARD_NAVIGATION_MENU_ITEMS = {
|
||||
allCompanies: {
|
||||
universalIdentifier: '20202020-b001-4b01-8b01-c0aba11c0001',
|
||||
viewUniversalIdentifier:
|
||||
STANDARD_OBJECTS.company.views.allCompanies.universalIdentifier,
|
||||
position: 0,
|
||||
},
|
||||
allDashboards: {
|
||||
universalIdentifier: '20202020-b002-4b02-8b02-c0aba11c0002',
|
||||
viewUniversalIdentifier:
|
||||
STANDARD_OBJECTS.dashboard.views.allDashboards.universalIdentifier,
|
||||
position: 1,
|
||||
},
|
||||
allNotes: {
|
||||
universalIdentifier: '20202020-b003-4b03-8b03-c0aba11c0003',
|
||||
viewUniversalIdentifier:
|
||||
STANDARD_OBJECTS.note.views.allNotes.universalIdentifier,
|
||||
position: 2,
|
||||
},
|
||||
allOpportunities: {
|
||||
universalIdentifier: '20202020-b004-4b04-8b04-c0aba11c0004',
|
||||
viewUniversalIdentifier:
|
||||
STANDARD_OBJECTS.opportunity.views.allOpportunities.universalIdentifier,
|
||||
position: 3,
|
||||
},
|
||||
allPeople: {
|
||||
universalIdentifier: '20202020-b005-4b05-8b05-c0aba11c0005',
|
||||
viewUniversalIdentifier:
|
||||
STANDARD_OBJECTS.person.views.allPeople.universalIdentifier,
|
||||
position: 4,
|
||||
},
|
||||
allTasks: {
|
||||
universalIdentifier: '20202020-b006-4b06-8b06-c0aba11c0006',
|
||||
viewUniversalIdentifier:
|
||||
STANDARD_OBJECTS.task.views.allTasks.universalIdentifier,
|
||||
position: 5,
|
||||
},
|
||||
allWorkflows: {
|
||||
universalIdentifier: '20202020-b007-4b07-8b07-c0aba11c0007',
|
||||
viewUniversalIdentifier:
|
||||
STANDARD_OBJECTS.workflow.views.allWorkflows.universalIdentifier,
|
||||
position: 6,
|
||||
},
|
||||
} as const;
|
||||
+1
@@ -8,6 +8,7 @@ export const TWENTY_STANDARD_ALL_METADATA_NAME = [
|
||||
'viewFilter',
|
||||
'viewGroup',
|
||||
'view',
|
||||
'navigationMenuItem',
|
||||
'role',
|
||||
'agent',
|
||||
'skill',
|
||||
|
||||
+77
@@ -3,10 +3,12 @@ import { Injectable } from '@nestjs/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-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';
|
||||
@@ -26,6 +28,7 @@ export class TwentyStandardApplicationService {
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
// Note: To remove and handle natively in validateBuildAndRun after favorite migration to metadata
|
||||
@@ -58,6 +61,80 @@ export class TwentyStandardApplicationService {
|
||||
);
|
||||
}
|
||||
|
||||
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.byId,
|
||||
).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,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while creating workspace navigation menu items',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async synchronizeTwentyStandardApplicationOrThrow({
|
||||
workspaceId,
|
||||
}: {
|
||||
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
@@ -14,6 +15,7 @@ import { TwentyStandardApplicationService } from './services/twenty-standard-app
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceMigrationModule,
|
||||
GlobalWorkspaceDataSourceModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
exports: [TwentyStandardApplicationService],
|
||||
})
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { addFlatNavigationMenuItemToMapsAndUpdateIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/add-flat-navigation-menu-item-to-maps-and-update-index.util';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { STANDARD_NAVIGATION_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-navigation-menu-item.constant';
|
||||
import { createStandardNavigationMenuItemFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/navigation-menu-item/create-standard-navigation-menu-item-flat-metadata.util';
|
||||
|
||||
export const buildStandardFlatNavigationMenuItemMaps = ({
|
||||
now,
|
||||
workspaceId,
|
||||
twentyStandardApplicationId,
|
||||
dependencyFlatEntityMaps: { flatViewMaps },
|
||||
}: {
|
||||
now: string;
|
||||
workspaceId: string;
|
||||
twentyStandardApplicationId: string;
|
||||
dependencyFlatEntityMaps: {
|
||||
flatViewMaps: FlatEntityMaps<FlatView>;
|
||||
};
|
||||
}): FlatNavigationMenuItemMaps => {
|
||||
const flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps = {
|
||||
...createEmptyFlatEntityMaps(),
|
||||
byUserWorkspaceIdAndFolderId: {},
|
||||
};
|
||||
|
||||
for (const navigationMenuItemName of Object.keys(
|
||||
STANDARD_NAVIGATION_MENU_ITEMS,
|
||||
) as Array<keyof typeof STANDARD_NAVIGATION_MENU_ITEMS>) {
|
||||
const navigationMenuItemDefinition =
|
||||
STANDARD_NAVIGATION_MENU_ITEMS[navigationMenuItemName];
|
||||
|
||||
const flatNavigationMenuItem = createStandardNavigationMenuItemFlatMetadata(
|
||||
{
|
||||
workspaceId,
|
||||
navigationMenuItemName,
|
||||
viewUniversalIdentifier:
|
||||
navigationMenuItemDefinition.viewUniversalIdentifier,
|
||||
position: navigationMenuItemDefinition.position,
|
||||
navigationMenuItemId: v4(),
|
||||
dependencyFlatEntityMaps: {
|
||||
flatViewMaps,
|
||||
},
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
},
|
||||
);
|
||||
|
||||
addFlatNavigationMenuItemToMapsAndUpdateIndex({
|
||||
flatNavigationMenuItem,
|
||||
flatNavigationMenuItemMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return flatNavigationMenuItemMaps;
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { STANDARD_NAVIGATION_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-navigation-menu-item.constant';
|
||||
|
||||
export const createStandardNavigationMenuItemFlatMetadata = ({
|
||||
workspaceId,
|
||||
navigationMenuItemName,
|
||||
viewUniversalIdentifier,
|
||||
position,
|
||||
navigationMenuItemId,
|
||||
dependencyFlatEntityMaps: { flatViewMaps },
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
navigationMenuItemName: keyof typeof STANDARD_NAVIGATION_MENU_ITEMS;
|
||||
viewUniversalIdentifier: string;
|
||||
position: number;
|
||||
navigationMenuItemId: string;
|
||||
dependencyFlatEntityMaps: {
|
||||
flatViewMaps: FlatEntityMaps<FlatView>;
|
||||
};
|
||||
twentyStandardApplicationId: string;
|
||||
now: string;
|
||||
}): FlatNavigationMenuItem => {
|
||||
const navigationMenuItemDefinition =
|
||||
STANDARD_NAVIGATION_MENU_ITEMS[navigationMenuItemName];
|
||||
|
||||
if (!isDefined(navigationMenuItemDefinition)) {
|
||||
throw new Error(
|
||||
`Invalid navigation menu item configuration ${navigationMenuItemName}`,
|
||||
);
|
||||
}
|
||||
|
||||
const flatView = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatViewMaps,
|
||||
universalIdentifier: viewUniversalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatView)) {
|
||||
throw new Error(
|
||||
`View not found for universal identifier ${viewUniversalIdentifier}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
id: navigationMenuItemId,
|
||||
universalIdentifier: navigationMenuItemDefinition.universalIdentifier,
|
||||
applicationId: twentyStandardApplicationId,
|
||||
workspaceId,
|
||||
userWorkspaceId: null,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
viewId: flatView.id,
|
||||
folderId: null,
|
||||
name: null,
|
||||
position,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
};
|
||||
+11
@@ -5,6 +5,7 @@ import { buildStandardFlatFieldMetadataMaps } from 'src/engine/workspace-manager
|
||||
import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util';
|
||||
import { getStandardPageLayoutMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-page-layout-metadata-related-entity-ids.util';
|
||||
import { buildStandardFlatIndexMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/build-standard-flat-index-metadata-maps.util';
|
||||
import { buildStandardFlatNavigationMenuItemMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/navigation-menu-item/build-standard-flat-navigation-menu-item-maps.util';
|
||||
import { buildStandardFlatObjectMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/build-standard-flat-object-metadata-maps.util';
|
||||
import { buildStandardFlatPageLayoutTabMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/build-standard-flat-page-layout-tab-metadata-maps.util';
|
||||
import { buildStandardFlatPageLayoutWidgetMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/build-standard-flat-page-layout-widget-metadata-maps.util';
|
||||
@@ -161,6 +162,15 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
|
||||
standardPageLayoutMetadataRelatedEntityIds,
|
||||
});
|
||||
|
||||
const flatNavigationMenuItemMaps = buildStandardFlatNavigationMenuItemMaps({
|
||||
now,
|
||||
workspaceId,
|
||||
twentyStandardApplicationId,
|
||||
dependencyFlatEntityMaps: {
|
||||
flatViewMaps,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
flatViewFieldMaps,
|
||||
flatViewFilterMaps,
|
||||
@@ -169,6 +179,7 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
|
||||
flatIndexMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatNavigationMenuItemMaps,
|
||||
flatRoleMaps,
|
||||
flatAgentMaps,
|
||||
flatSkillMaps,
|
||||
|
||||
Reference in New Issue
Block a user