[DASHBOARDS] Fix dashboard duplication createdBy (#16999)
Fixes https://github.com/twentyhq/core-team-issues/issues/2032 ## Before https://github.com/user-attachments/assets/5804d3e7-1c03-4eb9-9203-64656438f5d1 ## After https://github.com/user-attachments/assets/9f96458c-d2a5-481a-b6b2-4d88f0daa98a
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { DashboardSyncService } from 'src/modules/dashboard-sync/services/dashboard-sync.service';
|
||||
|
||||
@Module({
|
||||
imports: [TwentyORMModule, WorkspaceManyOrAllFlatEntityMapsCacheModule],
|
||||
providers: [DashboardSyncService],
|
||||
exports: [DashboardSyncService],
|
||||
})
|
||||
export class DashboardSyncModule {}
|
||||
@@ -0,0 +1,256 @@
|
||||
import { Injectable, Logger } 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 { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardSyncService {
|
||||
private readonly logger = new Logger(DashboardSyncService.name);
|
||||
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
private async isPageLayoutOfTypeDashboard({
|
||||
pageLayoutId,
|
||||
workspaceId,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<boolean> {
|
||||
const { flatPageLayoutMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const pageLayout = flatPageLayoutMaps.byId[pageLayoutId];
|
||||
|
||||
return (
|
||||
isDefined(pageLayout) && pageLayout.type === PageLayoutType.DASHBOARD
|
||||
);
|
||||
}
|
||||
|
||||
async updateLinkedDashboardsUpdatedAtByPageLayoutId({
|
||||
pageLayoutId,
|
||||
workspaceId,
|
||||
updatedAt,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
workspaceId: string;
|
||||
updatedAt: Date;
|
||||
}): Promise<void> {
|
||||
const isDashboard = await this.isPageLayoutOfTypeDashboard({
|
||||
pageLayoutId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!isDashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
try {
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const dashboardRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository(
|
||||
workspaceId,
|
||||
'dashboard',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
await dashboardRepository.update({ pageLayoutId }, { updatedAt });
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to update dashboard updatedAt for page layout ${pageLayoutId}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async softDeleteLinkedDashboardsByPageLayoutId({
|
||||
pageLayoutId,
|
||||
workspaceId,
|
||||
deletedAt,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
workspaceId: string;
|
||||
deletedAt: Date;
|
||||
}): Promise<void> {
|
||||
const isDashboard = await this.isPageLayoutOfTypeDashboard({
|
||||
pageLayoutId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!isDashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
try {
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const dashboardRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository(
|
||||
workspaceId,
|
||||
'dashboard',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
await dashboardRepository.update({ pageLayoutId }, { deletedAt });
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to soft delete dashboards for page layout ${pageLayoutId}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async restoreLinkedDashboardsByPageLayoutId({
|
||||
pageLayoutId,
|
||||
workspaceId,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
const isDashboard = await this.isPageLayoutOfTypeDashboard({
|
||||
pageLayoutId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!isDashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
try {
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const dashboardRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository(
|
||||
workspaceId,
|
||||
'dashboard',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
await dashboardRepository.update(
|
||||
{ pageLayoutId },
|
||||
{ deletedAt: null },
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to restore dashboards for page layout ${pageLayoutId}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async updateLinkedDashboardsUpdatedAtByTabId({
|
||||
tabId,
|
||||
workspaceId,
|
||||
updatedAt,
|
||||
}: {
|
||||
tabId: string;
|
||||
workspaceId: string;
|
||||
updatedAt: Date;
|
||||
}): Promise<void> {
|
||||
const { flatPageLayoutTabMaps, flatPageLayoutMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutTabMaps', 'flatPageLayoutMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const tab = flatPageLayoutTabMaps.byId[tabId];
|
||||
|
||||
if (!isDefined(tab)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageLayout = flatPageLayoutMaps.byId[tab.pageLayoutId];
|
||||
|
||||
if (
|
||||
!isDefined(pageLayout) ||
|
||||
pageLayout.type !== PageLayoutType.DASHBOARD
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.updateLinkedDashboardsUpdatedAtByPageLayoutId({
|
||||
pageLayoutId: tab.pageLayoutId,
|
||||
workspaceId,
|
||||
updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
async updateLinkedDashboardsUpdatedAtByWidgetId({
|
||||
widgetId,
|
||||
workspaceId,
|
||||
updatedAt,
|
||||
}: {
|
||||
widgetId: string;
|
||||
workspaceId: string;
|
||||
updatedAt: Date;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutMaps,
|
||||
} =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const widget = flatPageLayoutWidgetMaps.byId[widgetId];
|
||||
|
||||
if (!isDefined(widget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tab = flatPageLayoutTabMaps.byId[widget.pageLayoutTabId];
|
||||
|
||||
if (!isDefined(tab)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageLayout = flatPageLayoutMaps.byId[tab.pageLayoutId];
|
||||
|
||||
if (
|
||||
!isDefined(pageLayout) ||
|
||||
pageLayout.type !== PageLayoutType.DASHBOARD
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.updateLinkedDashboardsUpdatedAtByPageLayoutId({
|
||||
pageLayoutId: tab.pageLayoutId,
|
||||
workspaceId,
|
||||
updatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Controller, Param, Post, UseFilters, UseGuards } from '@nestjs/common';
|
||||
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
@@ -21,10 +26,17 @@ export class DashboardController {
|
||||
async duplicate(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUser() user: UserEntity,
|
||||
@AuthWorkspaceMemberId() workspaceMemberId: string,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
): Promise<DuplicatedDashboardDTO> {
|
||||
return this.dashboardDuplicationService.duplicateDashboard(
|
||||
id,
|
||||
workspace.id,
|
||||
);
|
||||
const authContext: AuthContext = {
|
||||
user,
|
||||
workspace,
|
||||
workspaceMemberId,
|
||||
userWorkspaceId,
|
||||
};
|
||||
|
||||
return this.dashboardDuplicationService.duplicateDashboard(id, authContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { PageLayoutModule } from 'src/engine/metadata-modules/page-layout/page-layout.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
@@ -10,6 +11,7 @@ import { DashboardDuplicationService } from 'src/modules/dashboard/services/dash
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ActorModule,
|
||||
AuthModule,
|
||||
PageLayoutModule,
|
||||
TwentyORMModule,
|
||||
|
||||
@@ -2,8 +2,13 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
@@ -29,10 +34,17 @@ export class DashboardResolver {
|
||||
async duplicateDashboard(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUser() user: UserEntity,
|
||||
@AuthWorkspaceMemberId() workspaceMemberId: string,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
): Promise<DuplicatedDashboardDTO> {
|
||||
return this.dashboardDuplicationService.duplicateDashboard(
|
||||
id,
|
||||
workspace.id,
|
||||
);
|
||||
const authContext: AuthContext = {
|
||||
user,
|
||||
workspace,
|
||||
workspaceMemberId,
|
||||
userWorkspaceId,
|
||||
};
|
||||
|
||||
return this.dashboardDuplicationService.duplicateDashboard(id, authContext);
|
||||
}
|
||||
}
|
||||
|
||||
+35
-9
@@ -1,11 +1,19 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { appendCopySuffix, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
appendCopySuffix,
|
||||
assertIsDefinedOrThrow,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
|
||||
|
||||
import { ActorFromAuthContextService } from 'src/engine/core-modules/actor/services/actor-from-auth-context.service';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { PageLayoutDuplicationService } from 'src/engine/metadata-modules/page-layout/services/page-layout-duplication.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { DuplicatedDashboardDTO } from 'src/modules/dashboard/dtos/duplicated-dashboard.dto';
|
||||
import {
|
||||
DashboardException,
|
||||
@@ -22,14 +30,21 @@ export class DashboardDuplicationService {
|
||||
constructor(
|
||||
private readonly pageLayoutDuplicationService: PageLayoutDuplicationService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly actorFromAuthContextService: ActorFromAuthContextService,
|
||||
) {}
|
||||
|
||||
async duplicateDashboard(
|
||||
dashboardId: string,
|
||||
workspaceId: string,
|
||||
authContext: AuthContext,
|
||||
): Promise<DuplicatedDashboardDTO> {
|
||||
const { workspace } = authContext;
|
||||
|
||||
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
|
||||
|
||||
const workspaceId = workspace.id;
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
buildSystemAuthContext(workspaceId),
|
||||
authContext as WorkspaceAuthContext,
|
||||
async () => {
|
||||
const dashboardRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<DashboardWorkspaceEntity>(
|
||||
@@ -73,6 +88,7 @@ export class DashboardDuplicationService {
|
||||
originalDashboard,
|
||||
newPageLayout.id,
|
||||
dashboardRepository,
|
||||
authContext,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -99,14 +115,24 @@ export class DashboardDuplicationService {
|
||||
originalDashboard: DashboardWorkspaceEntity,
|
||||
newPageLayoutId: string,
|
||||
dashboardRepository: WorkspaceRepository<DashboardWorkspaceEntity>,
|
||||
authContext: AuthContext,
|
||||
): Promise<DashboardWorkspaceEntity> {
|
||||
const newTitle = appendCopySuffix(originalDashboard.title ?? '');
|
||||
|
||||
const insertResult = await dashboardRepository.insert({
|
||||
title: newTitle,
|
||||
pageLayoutId: newPageLayoutId,
|
||||
position: originalDashboard.position,
|
||||
});
|
||||
const [recordWithActor] =
|
||||
await this.actorFromAuthContextService.injectActorFieldsOnCreate({
|
||||
records: [
|
||||
{
|
||||
title: newTitle,
|
||||
pageLayoutId: newPageLayoutId,
|
||||
position: originalDashboard.position,
|
||||
},
|
||||
],
|
||||
objectMetadataNameSingular: 'dashboard',
|
||||
authContext,
|
||||
});
|
||||
|
||||
const insertResult = await dashboardRepository.insert(recordWithActor);
|
||||
|
||||
const newDashboardId = insertResult.identifiers[0].id;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user