Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Task Linked Issue Activity Log #8514

Merged
merged 15 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/contracts/src/base-entity.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,6 @@ export enum BaseEntityEnum {
OrganizationVendor = 'OrganizationVendor',
Task = 'Task',
TaskView = 'TaskView',
TaskLinkedIssue = 'TaskLinkedIssue',
User = 'User'
}
16 changes: 7 additions & 9 deletions packages/contracts/src/task-linked-issue.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IBasePerTenantAndOrganizationEntityModel } from './base-entity.model';
import { IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model';
import { ITask } from './task.model';

export enum TaskRelatedIssuesRelationEnum {
Expand All @@ -8,23 +8,21 @@ export enum TaskRelatedIssuesRelationEnum {
CLONES = 4,
IS_DUPLICATED_BY = 5,
DUPLICATES = 6,
RELATES_TO = 7,
RELATES_TO = 7
}

export interface ITaskLinkedIssue
extends IBasePerTenantAndOrganizationEntityModel {
export interface ITaskLinkedIssue extends IBasePerTenantAndOrganizationEntityModel {
action: TaskRelatedIssuesRelationEnum;
taskFrom?: ITask;
taskFromId: ITask['id'];
taskFromId: ID;
taskTo?: ITask;
taskToId: ITask['id'];
taskToId: ID;
}

export interface ITaskLinkedIssueCreateInput extends ITaskLinkedIssue {}

export interface ITaskLinkedIssueUpdateInput
extends Partial<ITaskLinkedIssueCreateInput> {
id?: string;
export interface ITaskLinkedIssueUpdateInput extends Partial<ITaskLinkedIssueCreateInput> {
id?: ID;
}

export interface ILinkedIssueFindInput
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/activity-log/activity-log.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class ActivityLogService extends TenantAwareCrudService<ActivityLog> {
/**
* @description Create or Update Activity Log
* @template T
* @param {BaseEntityEnum} entityType - Entity type for whom creating activity log (E.g : Task, OrganizationProject, etc.)
* @param {BaseEntityEnum} entity - Entity type for whom creating activity log (E.g : Task, OrganizationProject, etc.)
* @param {string} entityName - Name or Title of the entity
* @param {ActorTypeEnum} actor - The actor type performing the action (User or System)
* @param {ID} organizationId
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { TaskLinkedIssueCreateHandler } from './task-linked-issue-create.handler';
import { TaskLinkedIssueUpdateHandler } from './task-linked-issue-update.handler';

export const CommandHandlers = [TaskLinkedIssueCreateHandler, TaskLinkedIssueUpdateHandler];
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { ITaskLinkedIssue } from '@gauzy/contracts';
import { TaskLinkedIssueCreateCommand } from '../task-linked-issue-create.command';
import { TaskLinkedIssueService } from '../../task-linked-issue.service';

@CommandHandler(TaskLinkedIssueCreateCommand)
export class TaskLinkedIssueCreateHandler implements ICommandHandler<TaskLinkedIssueCreateCommand> {
constructor(private readonly taskLinkedIssueService: TaskLinkedIssueService) {}

public async execute(command: TaskLinkedIssueCreateCommand): Promise<ITaskLinkedIssue> {
const { input } = command;

return await this.taskLinkedIssueService.create(input);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { ITaskLinkedIssue } from '@gauzy/contracts';
import { TaskLinkedIssueUpdateCommand } from '../task-linked-issue-update.command';
import { TaskLinkedIssueService } from '../../task-linked-issue.service';

@CommandHandler(TaskLinkedIssueUpdateCommand)
export class TaskLinkedIssueUpdateHandler implements ICommandHandler<TaskLinkedIssueUpdateCommand> {
constructor(private readonly taskLinkedIssueService: TaskLinkedIssueService) {}

public async execute(command: TaskLinkedIssueUpdateCommand): Promise<ITaskLinkedIssue> {
const { id, input } = command;

return await this.taskLinkedIssueService.update(id, input);
}
}
2 changes: 2 additions & 0 deletions packages/core/src/tasks/linked-issue/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './task-linked-issue-create.command';
export * from './task-linked-issue-update.command';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ICommand } from '@nestjs/cqrs';
import { ITaskLinkedIssueCreateInput } from '@gauzy/contracts';

export class TaskLinkedIssueCreateCommand implements ICommand {
static readonly type = '[Task Linked Issue] Create';

constructor(public readonly input: ITaskLinkedIssueCreateInput) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ICommand } from '@nestjs/cqrs';
import { ID, ITaskLinkedIssueUpdateInput } from '@gauzy/contracts';

export class TaskLinkedIssueUpdateCommand implements ICommand {
static readonly type = '[Task Linked Issue] Update';

constructor(public readonly id: ID, public readonly input: ITaskLinkedIssueUpdateInput) {}
}
22 changes: 6 additions & 16 deletions packages/core/src/tasks/linked-issue/dto/task-linked-issue.dto.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
import { TaskRelatedIssuesRelationEnum } from '@gauzy/contracts';
import { ApiProperty } from '@nestjs/swagger';
import { ITaskLinkedIssue } from '@gauzy/contracts';
import { IntersectionType } from '@nestjs/swagger';
import { TenantOrganizationBaseDTO } from '../../../core/dto';
import { IsEnum, IsUUID } from 'class-validator';
import { TaskLinkedIssue } from '../task-linked-issue.entity';

export class TaskLinkedIssueDTO extends TenantOrganizationBaseDTO {
@ApiProperty({ type: () => String, enum: TaskRelatedIssuesRelationEnum })
@IsEnum(TaskRelatedIssuesRelationEnum)
action: TaskRelatedIssuesRelationEnum;

@ApiProperty({ type: () => String })
@IsUUID()
taskFromId: string;

@ApiProperty({ type: () => String })
@IsUUID()
taskToId: string;
}
export class TaskLinkedIssueDTO
extends IntersectionType(TenantOrganizationBaseDTO, TaskLinkedIssue)
implements ITaskLinkedIssue {}
145 changes: 126 additions & 19 deletions packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,162 @@
import { Body, Controller, HttpCode, HttpStatus, Param, Post, Put, UseGuards } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ITaskLinkedIssue, PermissionsEnum } from '@gauzy/contracts';
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Put,
Query,
UseGuards
} from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { DeleteResult } from 'typeorm';
import { ID, IPagination, ITaskLinkedIssue, PermissionsEnum } from '@gauzy/contracts';
import { CrudController, PaginationParams } from '../../core/crud';
import { PermissionGuard, TenantPermissionGuard } from '../../shared/guards';
import { UUIDValidationPipe, UseValidationPipe } from '../../shared/pipes';
import { Permissions } from '../../shared/decorators';
import { CrudController } from '../../core/crud';
import { TaskLinkedIssue } from './task-linked-issue.entity';
import { TaskLinkedIssueService } from './task-linked-issue.service';
import { CreateTaskLinkedIssueDTO, UpdateTaskLinkedIssueDTO } from './dto';
import { TaskLinkedIssueCreateCommand, TaskLinkedIssueUpdateCommand } from './commands';

@ApiTags('Linked Issue')
@UseGuards(TenantPermissionGuard, PermissionGuard)
@Permissions(PermissionsEnum.ALL_ORG_EDIT)
@Permissions(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TASK_EDIT)
@Controller()
export class TaskLinkedIssueController extends CrudController<TaskLinkedIssue> {
constructor(protected readonly taskLinkedIssueService: TaskLinkedIssueService) {
constructor(
private readonly taskLinkedIssueService: TaskLinkedIssueService,
private readonly commandBus: CommandBus
) {
super(taskLinkedIssueService);
}

/**
* Create new Linked Issue
* Finds all task linked issues based on the provided query parameters.
*
* @param entity
* @returns
* @param params - The pagination and filter parameters for the query.
* @returns A promise that resolves to a paginated list of task linked issues.
*/
@ApiOperation({
summary: 'Find all'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Found task linked issues',
type: TaskLinkedIssue
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Record not found'
})
@Permissions(PermissionsEnum.ALL_ORG_VIEW, PermissionsEnum.ORG_TASK_VIEW)
@Get()
@UseValidationPipe()
async findAll(@Query() params: PaginationParams<TaskLinkedIssue>): Promise<IPagination<ITaskLinkedIssue>> {
return this.taskLinkedIssueService.findAll(params);
}

/**
* Creates a new task linked issue.
*
* @param entity - The input data for creating a task linked issue.
* @returns A promise that resolves to the created task linked issue.
*/
@ApiOperation({ summary: 'Create Task Linked Issue' })
@ApiResponse({
status: HttpStatus.CREATED,
description: 'The record has been successfully created.'
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Invalid input. The response body may contain clues as to what went wrong.'
})
@HttpCode(HttpStatus.CREATED)
@Permissions(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TASK_ADD)
@Post()
@UseValidationPipe({ whitelist: true })
async create(@Body() entity: CreateTaskLinkedIssueDTO): Promise<ITaskLinkedIssue> {
return await this.taskLinkedIssueService.create(entity);
return this.commandBus.execute(new TaskLinkedIssueCreateCommand(entity));
}

/**
* Update existing Linked Issue
* Updates an existing task linked issue.
*
* @param id
* @param entity
* @returns
* @param id - The ID of the task linked issue to update.
* @param entity - The input data for updating the task linked issue.
* @returns A promise that resolves to the updated task linked issue.
*/
@ApiOperation({ summary: 'Update an existing task linked issue' })
@ApiResponse({
status: HttpStatus.CREATED,
description: 'The record has been successfully edited.'
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Record not found'
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Invalid input. The response body may contain clues as to what went wrong.'
})
@HttpCode(HttpStatus.ACCEPTED)
@Permissions(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TASK_EDIT)
@Put(':id')
@UseValidationPipe({ whitelist: true })
async update(
@Param('id', UUIDValidationPipe) id: ITaskLinkedIssue['id'],
@Param('id', UUIDValidationPipe) id: ID,
@Body() entity: UpdateTaskLinkedIssueDTO
): Promise<ITaskLinkedIssue> {
return await this.taskLinkedIssueService.create({
...entity,
id
});
return this.commandBus.execute(new TaskLinkedIssueUpdateCommand(id, entity));
}

/**
* Deletes a task linked issue.
*
* @param id - The ID of the task linked issue to delete.
* @returns A promise that resolves to the result of the delete operation.
*/
@ApiOperation({ summary: 'Delete Task Linked Issue' })
@ApiResponse({
status: HttpStatus.NO_CONTENT,
description: 'The record has been successfully deleted'
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Record not found'
})
@HttpCode(HttpStatus.ACCEPTED)
@Permissions(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TASK_DELETE)
@Delete(':id')
async delete(@Param('id', UUIDValidationPipe) id: ID): Promise<DeleteResult> {
return this.taskLinkedIssueService.delete(id);
}

Comment on lines +136 to +138
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance delete method implementation

The delete method should handle potential errors and consider the following improvements:

  1. Add error handling for cases where the task might be referenced elsewhere
  2. Consider returning void to match NO_CONTENT status code
-async delete(@Param('id', UUIDValidationPipe) id: ID): Promise<DeleteResult> {
-    return this.taskLinkedIssueService.delete(id);
+async delete(@Param('id', UUIDValidationPipe) id: ID): Promise<void> {
+    try {
+        await this.taskLinkedIssueService.delete(id);
+    } catch (error) {
+        if (error.code === '23503') { // Foreign key violation
+            throw new HttpException(
+                'Cannot delete task linked issue as it is referenced by other entities',
+                HttpStatus.CONFLICT
+            );
+        }
+        throw error;
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async delete(@Param('id', UUIDValidationPipe) id: ID): Promise<DeleteResult> {
return this.taskLinkedIssueService.delete(id);
}
async delete(@Param('id', UUIDValidationPipe) id: ID): Promise<void> {
try {
await this.taskLinkedIssueService.delete(id);
} catch (error) {
if (error.code === '23503') { // Foreign key violation
throw new HttpException(
'Cannot delete task linked issue as it is referenced by other entities',
HttpStatus.CONFLICT
);
}
throw error;
}
}

/**
* Soft deletes a task linked issue record.
*
* @param id - The ID of the task linked issue to soft delete.
* @returns A promise that resolves to the result of the soft delete operation.
*/
@ApiOperation({ summary: 'Soft delete Task Linked Issue record' })
@ApiResponse({
status: HttpStatus.OK,
description: 'The record has been successfully soft-deleted'
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Task Linked Issue record not found'
})
@HttpCode(HttpStatus.ACCEPTED)
@Permissions(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TASK_DELETE)
@Delete(':id/soft')
@UseValidationPipe({ whitelist: true })
async softRemove(@Param('id', UUIDValidationPipe) id: ID): Promise<any> {
return this.taskLinkedIssueService.softDelete(id);
}
Comment on lines +159 to +160
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve softRemove method type safety and error handling

The method uses 'any' as return type and lacks error handling.

-async softRemove(@Param('id', UUIDValidationPipe) id: ID): Promise<any> {
-    return this.taskLinkedIssueService.softDelete(id);
+async softRemove(@Param('id', UUIDValidationPipe) id: ID): Promise<void> {
+    try {
+        await this.taskLinkedIssueService.softDelete(id);
+    } catch (error) {
+        if (error.code === '23503') {
+            throw new HttpException(
+                'Cannot soft delete task linked issue as it is referenced by other entities',
+                HttpStatus.CONFLICT
+            );
+        }
+        throw error;
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async softRemove(@Param('id', UUIDValidationPipe) id: ID): Promise<any> {
return this.taskLinkedIssueService.softDelete(id);
async softRemove(@Param('id', UUIDValidationPipe) id: ID): Promise<void> {
try {
await this.taskLinkedIssueService.softDelete(id);
} catch (error) {
if (error.code === '23503') {
throw new HttpException(
'Cannot soft delete task linked issue as it is referenced by other entities',
HttpStatus.CONFLICT
);
}
throw error;
}
}

}
23 changes: 9 additions & 14 deletions packages/core/src/tasks/linked-issue/task-linked-issue.entity.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
JoinColumn,
RelationId,
} from 'typeorm';
import { IsEnum, IsUUID } from 'class-validator';
import {
ITask,
ITaskLinkedIssue,
TaskRelatedIssuesRelationEnum,
} from '@gauzy/contracts';
import { JoinColumn, RelationId } from 'typeorm';
import { IsEnum, IsOptional, IsUUID } from 'class-validator';
import { ID, ITask, ITaskLinkedIssue, TaskRelatedIssuesRelationEnum } from '@gauzy/contracts';
import { Task } from './../task.entity';
import { TenantOrganizationBaseEntity } from './../../core/entities/internal';
import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from './../../core/decorators/entity';
import { MikroOrmTaskLinkedIssueRepository } from './repository/mikro-orm-linked-issue.repository';

@MultiORMEntity('task_linked_issues', { mikroOrmRepository: () => MikroOrmTaskLinkedIssueRepository })
export class TaskLinkedIssue extends TenantOrganizationBaseEntity implements ITaskLinkedIssue {
@ApiProperty({ type: () => String, enum: TaskRelatedIssuesRelationEnum })
@MultiORMColumn()
@ApiProperty({ enum: TaskRelatedIssuesRelationEnum })
@IsEnum(TaskRelatedIssuesRelationEnum)
@MultiORMColumn()
action: TaskRelatedIssuesRelationEnum;

/*
Expand All @@ -27,6 +20,7 @@ export class TaskLinkedIssue extends TenantOrganizationBaseEntity implements ITa
|--------------------------------------------------------------------------
*/
@ApiPropertyOptional({ type: () => Task })
@IsOptional()
@MultiORMManyToOne(() => Task)
@JoinColumn()
taskFrom?: ITask;
Expand All @@ -36,12 +30,13 @@ export class TaskLinkedIssue extends TenantOrganizationBaseEntity implements ITa
@RelationId((it: TaskLinkedIssue) => it.taskFrom)
@ColumnIndex()
@MultiORMColumn({ relationId: true })
taskFromId: ITask['id'];
taskFromId: ID;

/**
* Task Linked Issues
*/
@ApiPropertyOptional({ type: () => Object })
@IsOptional()
@MultiORMManyToOne(() => Task, (it) => it.linkedIssues)
@JoinColumn()
taskTo?: ITask;
Expand All @@ -51,5 +46,5 @@ export class TaskLinkedIssue extends TenantOrganizationBaseEntity implements ITa
@RelationId((it: TaskLinkedIssue) => it.taskTo)
@ColumnIndex()
@MultiORMColumn({ relationId: true })
taskToId: ITask['id'];
taskToId: ID;
}
Loading
Loading