From c74b6816a1085402e675bcd45d04f1afb82cfd3a Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Sat, 26 Oct 2024 18:32:55 +0530 Subject: [PATCH 01/30] fix: soft delete time slots after merge new slots --- .../commands/create-time-slot.command.ts | 2 +- .../handlers/create-time-slot.handler.ts | 7 +- .../handlers/time-slot-merge.handler.ts | 130 +++++++++++------- .../commands/time-slot-merge.command.ts | 3 +- 4 files changed, 85 insertions(+), 57 deletions(-) diff --git a/packages/core/src/time-tracking/time-slot/commands/create-time-slot.command.ts b/packages/core/src/time-tracking/time-slot/commands/create-time-slot.command.ts index bbb32d29cdd..3a068004b1e 100644 --- a/packages/core/src/time-tracking/time-slot/commands/create-time-slot.command.ts +++ b/packages/core/src/time-tracking/time-slot/commands/create-time-slot.command.ts @@ -4,5 +4,5 @@ import { ITimeSlot } from '@gauzy/contracts'; export class CreateTimeSlotCommand implements ICommand { static readonly type = '[TimeSlot] create'; - constructor(public readonly input: ITimeSlot) {} + constructor(public readonly input: ITimeSlot, public readonly forceDelete: boolean = false) {} } diff --git a/packages/core/src/time-tracking/time-slot/commands/handlers/create-time-slot.handler.ts b/packages/core/src/time-tracking/time-slot/commands/handlers/create-time-slot.handler.ts index 68d33daae93..d5cbb041c11 100644 --- a/packages/core/src/time-tracking/time-slot/commands/handlers/create-time-slot.handler.ts +++ b/packages/core/src/time-tracking/time-slot/commands/handlers/create-time-slot.handler.ts @@ -36,7 +36,7 @@ export class CreateTimeSlotHandler implements ICommandHandler} - A promise that resolves to the created or updated TimeSlot instance. */ public async execute(command: CreateTimeSlotCommand): Promise { - const { input } = command; + const { input, forceDelete } = command; let { organizationId, employeeId, @@ -177,8 +177,11 @@ export class CreateTimeSlotHandler implements ICommandHandler 0) { - await this.typeOrmTimeSlotRepository.delete({ - id: In(ids) - }); - } + // Clean up old time slots if needed + await this.cleanUpOldTimeSlots(timeSlots, forceDelete); }) .values() .value(); @@ -149,43 +129,87 @@ export class TimeSlotMergeHandler implements ICommandHandler { - /** - * GET Time Slots for given date range slot - */ + // Create a query builder for the TimeSlot entity const query = this.typeOrmTimeSlotRepository.createQueryBuilder(); - query.leftJoinAndSelect(`${query.alias}.timeLogs`, 'timeLogs'); - query.leftJoinAndSelect(`${query.alias}.screenshots`, 'screenshots'); - query.leftJoinAndSelect(`${query.alias}.activities`, 'activities'); - query.where((qb: SelectQueryBuilder) => { - qb.andWhere(p(`"${qb.alias}"."startedAt" >= :startedAt AND "${qb.alias}"."startedAt" < :stoppedAt`), { + query + .leftJoinAndSelect(`${query.alias}.timeLogs`, 'timeLogs') + .leftJoinAndSelect(`${query.alias}.screenshots`, 'screenshots') + .leftJoinAndSelect(`${query.alias}.activities`, 'activities'); + query + .where(p(`"${query.alias}"."startedAt" >= :startedAt AND "${query.alias}"."startedAt" < :stoppedAt`), { startedAt, stoppedAt - }); - qb.andWhere(p(`"${qb.alias}"."employeeId" = :employeeId`), { - employeeId - }); - qb.andWhere(p(`"${qb.alias}"."organizationId" = :organizationId`), { - organizationId - }); - qb.andWhere(p(`"${qb.alias}"."tenantId" = :tenantId`), { - tenantId - }); - }); - query.addOrderBy(p(`"${query.alias}"."createdAt"`), 'ASC'); - const timeSlots = await query.getMany(); - return timeSlots; + }) + .andWhere(p(`"${query.alias}"."employeeId" = :employeeId`), { employeeId }) + .andWhere(p(`"${query.alias}"."organizationId" = :organizationId`), { organizationId }) + .andWhere(p(`"${query.alias}"."tenantId" = :tenantId`), { tenantId }) + .addOrderBy(p(`"${query.alias}"."createdAt"`), 'ASC'); + + // Execute the query and return the results + return await query.getMany(); } /** + * Updates time logs and recalculates the total worked hours for an employee based on the given time slot. * - * @param newTimeSlot + * @param newTimeSlot - The newly created time slot containing time logs and employee information. */ private async updateTimeLogAndEmployeeTotalWorkedHours(newTimeSlot: ITimeSlot): Promise { /** diff --git a/packages/core/src/time-tracking/time-slot/commands/time-slot-merge.command.ts b/packages/core/src/time-tracking/time-slot/commands/time-slot-merge.command.ts index 87e13635e98..e21d1b3e206 100644 --- a/packages/core/src/time-tracking/time-slot/commands/time-slot-merge.command.ts +++ b/packages/core/src/time-tracking/time-slot/commands/time-slot-merge.command.ts @@ -8,6 +8,7 @@ export class TimeSlotMergeCommand implements ICommand { public readonly organizationId: ID, public readonly employeeId: ID, public readonly start: Date, - public readonly end: Date + public readonly end: Date, + public readonly forceDelete: boolean = false ) {} } From f0c12790974812a9ff5a8ab763df0a51238b5b99 Mon Sep 17 00:00:00 2001 From: Ruslan Konviser Date: Sun, 27 Oct 2024 12:23:53 +0100 Subject: [PATCH 02/30] chore: no persist workspace in CircleCI --- .circleci/config.yml | 34 ++++------------------------------ 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 849f103540e..a4464eb5522 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -101,10 +101,7 @@ jobs: key: yarn-packages-sonarqube-root-{{ checksum "yarn.lock" }} paths: - ~/.cache/yarn - - persist_to_workspace: - root: /tmp/workspace/sonarqube-root - paths: - - '*' + build-monorepo-root: <<: *defaults machine: @@ -131,10 +128,7 @@ jobs: key: yarn-packages-monorepo-root-{{ checksum "yarn.lock" }} paths: - ~/.cache/yarn - - persist_to_workspace: - root: /tmp/workspace/monorepo-root - paths: - - '*' + build-desktop: <<: *defaults machine: @@ -165,10 +159,7 @@ jobs: key: yarn-packages-desktop-{{ checksum "yarn.lock" }} paths: - ~/.cache/yarn - - persist_to_workspace: - root: /tmp/workspace/desktop - paths: - - '*' + build-api: <<: *defaults machine: @@ -203,10 +194,7 @@ jobs: key: yarn-packages-api-{{ checksum "yarn.lock" }} paths: - ~/.cache/yarn - - persist_to_workspace: - root: /tmp/workspace/api - paths: - - '*' + build-web: <<: *defaults machine: @@ -241,10 +229,6 @@ jobs: key: yarn-packages-web-{{ checksum "yarn.lock" }} paths: - ~/.cache/yarn - - persist_to_workspace: - root: /tmp/workspace/web - paths: - - '*' test-e2e: <<: *defaults @@ -289,11 +273,6 @@ jobs: name: Kill API Background Process command: pgrep node &> /dev/null && killall -w node || true - - persist_to_workspace: - root: /tmp/workspace/test-e2e - paths: - - '*' - pulumi_deploy: <<: *defaults machine: @@ -333,11 +312,6 @@ jobs: paths: - ~/.cache/yarn - - persist_to_workspace: - root: /tmp/workspace/pulumi - paths: - - '*' - workflows: version: 2 build: From 2e1b45efb5c9f0a52fd13c08e4c1a755f8b5e18d Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Mon, 28 Oct 2024 11:05:40 +0530 Subject: [PATCH 03/30] fix: #8501 apply code rabbit suggestions --- .../handlers/time-slot-merge.handler.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/core/src/time-tracking/time-slot/commands/handlers/time-slot-merge.handler.ts b/packages/core/src/time-tracking/time-slot/commands/handlers/time-slot-merge.handler.ts index 9d2c11bd96d..4ae1b1f62d0 100644 --- a/packages/core/src/time-tracking/time-slot/commands/handlers/time-slot-merge.handler.ts +++ b/packages/core/src/time-tracking/time-slot/commands/handlers/time-slot-merge.handler.ts @@ -149,19 +149,24 @@ export class TimeSlotMergeHandler implements ICommandHandler Date: Mon, 28 Oct 2024 11:25:29 +0530 Subject: [PATCH 04/30] chore(deps): update yarn.lock --- yarn.lock | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index fae89488a87..4b150013d39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3866,9 +3866,9 @@ optionalDependencies: global-agent "^3.0.0" -"@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2": +"@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2": version "10.2.0-electron.1" - resolved "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2" + resolved "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2" dependencies: env-paths "^2.2.0" exponential-backoff "^3.1.1" @@ -14511,14 +14511,6 @@ better-sqlite3@11.3.0: bindings "^1.5.0" prebuild-install "^7.1.1" -better-sqlite3@^11.5.0: - version "11.5.0" - resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-11.5.0.tgz#58faa51e02845a578dd154f0083487132ead0695" - integrity sha512-e/6eggfOutzoK0JWiU36jsisdWoHOfN9iWiW/SieKvb7SAa6aGNmBM/UKyp+/wWSXpLlWNN8tCPwoDNPhzUvuQ== - dependencies: - bindings "^1.5.0" - prebuild-install "^7.1.1" - better-sqlite3@^9.5.0: version "9.6.0" resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-9.6.0.tgz#b01e58ba7c48abcdc0383b8301206ee2ab81d271" From 4e2db8166b80ad42ef648bdf8c330eee6bc62c04 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Mon, 28 Oct 2024 13:26:31 +0530 Subject: [PATCH 05/30] [Refactor] Time Slot Merge Handler (Soft Delete) (#8503) * fix: time slot merge handler soft delete previous slot * refactor: updated time slot merge handler * refactor: updated time slot merge handler * fix: apply code rabbit suggestions * fix: apply code rabbit suggestions * fix: apply code rabbit suggestions --- angular.json | 3 +- .../handlers/time-slot-merge.handler.ts | 308 ++++++++++++------ 2 files changed, 218 insertions(+), 93 deletions(-) diff --git a/angular.json b/angular.json index e778b0af4d0..ad69a64d7ea 100644 --- a/angular.json +++ b/angular.json @@ -85,7 +85,8 @@ "randomcolor", "underscore.string", "slugify", - "eva-icons" + "eva-icons", + "localforage" ] }, "configurations": { diff --git a/packages/core/src/time-tracking/time-slot/commands/handlers/time-slot-merge.handler.ts b/packages/core/src/time-tracking/time-slot/commands/handlers/time-slot-merge.handler.ts index 4ae1b1f62d0..bb00d444ff6 100644 --- a/packages/core/src/time-tracking/time-slot/commands/handlers/time-slot-merge.handler.ts +++ b/packages/core/src/time-tracking/time-slot/commands/handlers/time-slot-merge.handler.ts @@ -2,17 +2,27 @@ import { CommandBus, CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { In } from 'typeorm'; import * as moment from 'moment'; import { chain, omit, pluck, uniq } from 'underscore'; -import { DateRange, IActivity, IScreenshot, ITimeLog, ITimeSlot } from '@gauzy/contracts'; +import { DateRange, IActivity, ID, IScreenshot, ITimeLog, ITimeSlot } from '@gauzy/contracts'; import { isNotEmpty } from '@gauzy/common'; -import { TimeSlotMergeCommand } from '../time-slot-merge.command'; import { Activity, Screenshot, TimeSlot } from './../../../../core/entities/internal'; import { RequestContext } from './../../../../core/context'; import { getDateRangeFormat } from './../../../../core/utils'; +import { prepareSQLQuery as p } from './../../../../database/database.helper'; +import { TimeSlotMergeCommand } from '../time-slot-merge.command'; import { TimesheetRecalculateCommand } from './../../../timesheet/commands'; import { UpdateEmployeeTotalWorkedHoursCommand } from './../../../../employee/commands'; -import { prepareSQLQuery as p } from './../../../../database/database.helper'; import { TypeOrmTimeSlotRepository } from '../../repository/type-orm-time-slot.repository'; +interface IAggregatedTimeSlot { + duration: number; + keyboard: number; + mouse: number; + overall: number; + screenshots: IScreenshot[]; + timeLogs: ITimeLog[]; + activities: IActivity[]; +} + @CommandHandler(TimeSlotMergeCommand) export class TimeSlotMergeHandler implements ICommandHandler { constructor( @@ -25,108 +35,74 @@ export class TimeSlotMergeHandler implements ICommandHandler { + const { organizationId, employeeId, start, end, forceDelete } = command; const tenantId = RequestContext.currentTenantId(); // Round start and end dates to the nearest 10 minutes const { start: startedAt, end: stoppedAt } = this.getRoundedDateRange(start, end); - console.log({ startedAt, stoppedAt }, 'Time Slot Merging Dates'); - // GET Time Slots for the given date range slot - const timeSlots = await this.getTimeSlots({ + // Retrieve time slots for the given date range + const slots = await this.getTimeSlots({ organizationId, employeeId, tenantId, startedAt, stoppedAt }); + console.log('GET Time Slots To Be Merged Length: %s', slots.length); + + if (isNotEmpty(slots)) { + const groupedTimeSlots = this.groupTimeSlots(slots); // Group time slots by rounded start time - const createdTimeSlots: ITimeSlot[] = []; - - if (isNotEmpty(timeSlots)) { - const groupByTimeSlots = chain(timeSlots).groupBy((timeSlot) => { - let date = moment(timeSlot.startedAt); - const minutes = date.get('minute'); - date = date - .set('minute', minutes - (minutes % 10)) - .set('second', 0) - .set('millisecond', 0); - return date.format('YYYY-MM-DD HH:mm:ss'); - }); - const savePromises = groupByTimeSlots - .mapObject(async (timeSlots, slotStart) => { - const [timeSlot] = timeSlots; - - let timeLogs: ITimeLog[] = []; - let screenshots: IScreenshot[] = []; - let activities: IActivity[] = []; - - let duration = 0; - let keyboard = 0; - let mouse = 0; - let overall = 0; - - const calculateValue = (value: number | undefined): number => parseInt(value as any, 10) || 0; - - duration += timeSlots.reduce((acc, slot) => acc + calculateValue(slot.duration), 0); - keyboard += timeSlots.reduce((acc, slot) => acc + calculateValue(slot.keyboard), 0); - mouse += timeSlots.reduce((acc, slot) => acc + calculateValue(slot.mouse), 0); - overall += timeSlots.reduce((acc, slot) => acc + calculateValue(slot.overall), 0); - - screenshots = screenshots.concat(...timeSlots.map((slot) => slot.screenshots || [])); - timeLogs = timeLogs.concat(...timeSlots.map((slot) => slot.timeLogs || [])); - activities = activities.concat(...timeSlots.map((slot) => slot.activities || [])); - - const nonZeroKeyboardSlots = timeSlots.filter((item: ITimeSlot) => item.keyboard !== 0); - const timeSlotsLength = nonZeroKeyboardSlots.length; - - keyboard = Math.round(keyboard / timeSlotsLength || 0); - mouse = Math.round(mouse / timeSlotsLength || 0); - - const activity = { - duration: Math.max(0, Math.min(600, duration)), - overall: Math.max(0, Math.min(600, overall)), - keyboard: Math.max(0, Math.min(600, keyboard)), - mouse: Math.max(0, Math.min(600, mouse)) - }; - /* - * Map old screenshots newly created TimeSlot - */ - screenshots = screenshots.map((item) => new Screenshot(omit(item, ['timeSlotId']))); - /* - * Map old activities newly created TimeSlot - */ - activities = activities.map((item) => new Activity(omit(item, ['timeSlotId']))); - - timeLogs = uniq(timeLogs, (log: ITimeLog) => log.id); - - const newTimeSlot = new TimeSlot({ - ...omit(timeSlot), - ...activity, - screenshots, - activities, - timeLogs, - startedAt: moment(slotStart).toDate(), - tenantId, - organizationId, - employeeId - }); - console.log('Newly Created Time Slot', newTimeSlot); - - await this.updateTimeLogAndEmployeeTotalWorkedHours(newTimeSlot); - - await this.typeOrmTimeSlotRepository.save(newTimeSlot); - createdTimeSlots.push(newTimeSlot); - - // Clean up old time slots if needed - await this.cleanUpOldTimeSlots(timeSlots, forceDelete); - }) - .values() - .value(); - await Promise.all(savePromises); + // Aggregate data and save new time slots + return await this.mergeAndSaveTimeSlots( + groupedTimeSlots, // Group time slots by rounded start time + tenantId, + organizationId, + employeeId, + forceDelete + ); } - return createdTimeSlots; + + return []; + } + + /** + * Aggregates, saves new time slots, and deletes old ones. + * + * @param groupedTimeSlots - The grouped time slots by rounded start time + * @param tenantId - Tenant ID associated with the time slots + * @param organizationId - Organization ID associated with the time slots + * @param employeeId - Employee ID associated with the time slots + * @param forceDelete - Flag to force deletion of old time slots + * @returns An array of created time slots + */ + private async mergeAndSaveTimeSlots( + groupedTimeSlots: Record, + tenantId: ID, + organizationId: ID, + employeeId: ID, + forceDelete: boolean + ): Promise { + const newTimeSlots: ITimeSlot[] = []; + + await Promise.all( + Object.entries(groupedTimeSlots).map(async ([start, slots]) => { + // Create a new TimeSlot instance with aggregated data + const newTimeSlot = await this.createNewTimeSlot(slots, start, tenantId, organizationId, employeeId); + + // Update time logs and recalculate total worked hours for the employee + await this.updateTimeLogAndEmployeeTotalWorkedHours(newTimeSlot); + + // Clean up old time slots if needed + await this.cleanUpOldTimeSlots(slots, forceDelete); + + newTimeSlots.push(newTimeSlot); + }) + ); + + return newTimeSlots; } /** @@ -142,6 +118,57 @@ export class TimeSlotMergeHandler implements ICommandHandler { + return chain(timeSlots) + .groupBy((slot) => this.roundToNearestTenMinutes(moment(slot.startedAt)).format('YYYY-MM-DD HH:mm:ss')) + .value(); + } + + /** + * Creates a new TimeSlot instance by aggregating data from multiple time slots. + * Calculates the overall duration, keyboard, mouse, and activity metrics, and + * creates a new `TimeSlot` entity with aggregated screenshots, activities, and time logs. + * + * @param slots - The array of time slots to aggregate data from + * @param startedAt - The start time for the new aggregated time slot + * @param tenantId - The tenant ID associated with the time slot + * @param organizationId - The organization ID associated with the time slot + * @param employeeId - The employee ID associated with the time slot + * @returns A new `TimeSlot` instance with aggregated data + */ + private async createNewTimeSlot( + slots: ITimeSlot[], + startedAt: string, + tenantId: ID, + organizationId: ID, + employeeId: ID + ): Promise { + const [slot] = slots; // Get the first time slot and aggregate data from all time slots + const aggregated = this.aggregateTimeSlot(slots); // Aggregate data from all time slots + + // Create new TimeSlot instance with aggregated data + const newTimeSlot = new TimeSlot({ + ...omit(slot), + ...this.calculateActivity(aggregated, slots), // Calculate activity metrics + screenshots: this.mapScreenshots(aggregated.screenshots), // Map old screenshots + activities: this.mapActivities(aggregated.activities), // Map old activities + timeLogs: this.mapUniqueTimeLogs(aggregated.timeLogs), // Deduplicate time logs + startedAt: moment(startedAt).toDate(), + tenantId, + organizationId, + employeeId + }); + + console.log('Newly Created Time Slot with Aggregated Data:', newTimeSlot); + // Save the new time slot to the database + return await this.typeOrmTimeSlotRepository.save(newTimeSlot); + } + /** * Deletes or soft-deletes old time slots based on the `forceDelete` flag. * @@ -170,6 +197,102 @@ export class TimeSlotMergeHandler implements ICommandHandler { + acc.duration += this.calculateValue(slot.duration); + acc.keyboard += this.calculateValue(slot.keyboard); + acc.mouse += this.calculateValue(slot.mouse); + acc.overall += this.calculateValue(slot.overall); + acc.screenshots.push(...(slot.screenshots || [])); + acc.timeLogs.push(...(slot.timeLogs || [])); + acc.activities.push(...(slot.activities || [])); + return acc; + }, + { + duration: 0, + keyboard: 0, + mouse: 0, + overall: 0, + screenshots: [], + timeLogs: [], + activities: [] + } + ); + } + + /** + * Calculates the average activity metrics from the aggregated data. + * It calculates average keyboard and mouse activity from time slots that contain non-zero keyboard data. + * It also ensures each metric (duration, overall, keyboard, mouse) is capped at a maximum of 600. + * + * @param data - Aggregated data from time slots including total keyboard, mouse, and overall activities. + * @returns An object containing the calculated activity metrics (duration, overall, keyboard, mouse). + */ + private calculateActivity(data: IAggregatedTimeSlot, slots: ITimeSlot[]) { + const nonZeroKeyboardSlots = slots.filter((slot: ITimeSlot) => slot.keyboard > 0); + const count = nonZeroKeyboardSlots.length; // Count the number of non-zero keyboard slots + + const keyboardAverage = count > 0 ? Math.round(data.keyboard / count) : 0; + const mouseAverage = count > 0 ? Math.round(data.mouse / count) : 0; + + return { + duration: Math.max(0, Math.min(600, data.duration)), + overall: Math.max(0, Math.min(600, data.overall)), + keyboard: Math.max(0, Math.min(600, keyboardAverage)), + mouse: Math.max(0, Math.min(600, mouseAverage)) + }; + } + + /** + * Maps and prepares screenshots for a new time slot by omitting the `timeSlotId` property. + * + * @param screenshots - Array of screenshots to be mapped + * @returns A new array of `Screenshot` instances without `timeSlotId` + */ + private mapScreenshots(screenshots: IScreenshot[]): Screenshot[] { + return screenshots.map((screenshot) => new Screenshot(omit(screenshot, ['timeSlotId']))); + } + + /** + * Maps and prepares activities for a new time slot by omitting the `timeSlotId` property. + * + * @param activities - Array of activities to be mapped + * @returns A new array of `Activity` instances without `timeSlotId` + */ + private mapActivities(activities: IActivity[]): Activity[] { + return activities.map((activity) => new Activity(omit(activity, ['timeSlotId']))); + } + + /** + * Maps and deduplicates time logs by their unique ID. + * + * @param logs - Array of time logs to be mapped and deduplicated + * @returns Array of unique time logs + */ + private mapUniqueTimeLogs(logs: ITimeLog[]): ITimeLog[] { + return uniq(logs, (log: ITimeLog) => log.id); + } + + /** + * Calculates a value safely, returning 0 if the input is undefined or not a number. + * + * @param value - The value to calculate + * @returns The calculated value, or 0 if the input is undefined or invalid + */ + private calculateValue(value?: number): number { + return Number(value) || 0; + } + /** * Round a moment date to the nearest 10 minutes * @@ -207,6 +330,7 @@ export class TimeSlotMergeHandler implements ICommandHandler Date: Mon, 28 Oct 2024 15:12:14 +0200 Subject: [PATCH 06/30] [Fix] User removal from organization not functioning as expected --- .../edit-user-organizations.component.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/gauzy/src/app/pages/users/edit-user-profile/edit-user-organizations/edit-user-organizations.component.ts b/apps/gauzy/src/app/pages/users/edit-user-profile/edit-user-organizations/edit-user-organizations.component.ts index cf7dd512b3c..0f8bdd1c6cf 100644 --- a/apps/gauzy/src/app/pages/users/edit-user-profile/edit-user-organizations/edit-user-organizations.component.ts +++ b/apps/gauzy/src/app/pages/users/edit-user-profile/edit-user-organizations/edit-user-organizations.component.ts @@ -85,19 +85,20 @@ export class EditUserOrganizationsComponent extends TranslationBaseComponent imp async remove(id: string) { const { tenantId } = this.store.user; const user = await this.usersService.getUserById(this.selectedUserId); - const { items } = await this.userOrganizationsService.getAll(['user', 'user.role'], { tenantId }); + const { items } = await this.userOrganizationsService.getAll(['user', 'user.role'], { + tenantId, + userId: this.selectedUserId + }); let counter = 0; let userName: string; - for (const orgUser of items) { - if (orgUser.isActive && (!orgUser.user.role || orgUser.user.role.name !== RolesEnum.EMPLOYEE)) { - this.userToRemove = orgUser; - userName = orgUser.user.firstName + ' ' + orgUser.user.lastName; + this.userToRemove = items.find((orgUser) => orgUser.organizationId === id); - if (orgUser.organizationId === id) this.orgUserId = orgUser.id; - if (this.userToRemove.user.id === user.id) counter++; - } + if (this.userToRemove) { + userName = this.userToRemove.user.firstName + ' ' + this.userToRemove.user.lastName; + this.orgUserId = this.userToRemove.id; + counter = items.filter((orgUser) => orgUser.user.id === user.id && orgUser.isActive).length; } if (counter - 1 < 1) { From 7907cd2fa124e58e6905afc18638d3cb29ef0124 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 29 Oct 2024 11:51:30 +0200 Subject: [PATCH 07/30] fix: update entities include relations --- .../organization-project-module.service.ts | 10 ++++++---- .../organization-project.service.ts | 8 +++++++- .../organization-sprint/organization-sprint.service.ts | 10 ++++++---- .../core/src/resource-link/resource-link.service.ts | 7 ++++++- .../commands/handlers/automation-task.sync.handler.ts | 5 ++++- packages/core/src/tasks/task.service.ts | 6 +++++- 6 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/core/src/organization-project-module/organization-project-module.service.ts b/packages/core/src/organization-project-module/organization-project-module.service.ts index 6b7698de7c2..3395769e435 100644 --- a/packages/core/src/organization-project-module/organization-project-module.service.ts +++ b/packages/core/src/organization-project-module/organization-project-module.service.ts @@ -81,12 +81,14 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService relation.propertyName + ); + // Retrieve existing module. const existingProjectModule = await this.findOneByIdString(id, { - relations: { - members: true, - manager: true - } + relations }); if (!existingProjectModule) { diff --git a/packages/core/src/organization-project/organization-project.service.ts b/packages/core/src/organization-project/organization-project.service.ts index e9499e49735..883455f4227 100644 --- a/packages/core/src/organization-project/organization-project.service.ts +++ b/packages/core/src/organization-project/organization-project.service.ts @@ -155,8 +155,14 @@ export class OrganizationProjectService extends TenantAwareCrudService relation.propertyName + ); + let organizationProject = await super.findOneByIdString(id, { - where: { organizationId, tenantId } + where: { organizationId, tenantId }, + relations }); try { diff --git a/packages/core/src/organization-sprint/organization-sprint.service.ts b/packages/core/src/organization-sprint/organization-sprint.service.ts index 4b3600239cf..b588329efb4 100644 --- a/packages/core/src/organization-sprint/organization-sprint.service.ts +++ b/packages/core/src/organization-sprint/organization-sprint.service.ts @@ -148,13 +148,15 @@ export class OrganizationSprintService extends TenantAwareCrudService relation.propertyName + ); + // Search for existing Organization Sprint const organizationSprint = await super.findOneByIdString(id, { where: { organizationId, tenantId, projectId }, - relations: { - members: true, - modules: true - } + relations }); // Retrieve members and managers IDs diff --git a/packages/core/src/resource-link/resource-link.service.ts b/packages/core/src/resource-link/resource-link.service.ts index a4f43eb9164..57efffe39e4 100644 --- a/packages/core/src/resource-link/resource-link.service.ts +++ b/packages/core/src/resource-link/resource-link.service.ts @@ -80,7 +80,12 @@ export class ResourceLinkService extends TenantAwareCrudService { */ async update(id: ID, input: IResourceLinkUpdateInput): Promise { try { - const resourceLink = await this.findOneByIdString(id); + // Find Resource Link relations + const relations = this.typeOrmResourceLinkRepository.metadata.relations.map( + (relation) => relation.propertyName + ); + + const resourceLink = await this.findOneByIdString(id, { relations }); if (!resourceLink) { throw new BadRequestException('Resource Link not found'); diff --git a/packages/core/src/tasks/commands/handlers/automation-task.sync.handler.ts b/packages/core/src/tasks/commands/handlers/automation-task.sync.handler.ts index 05c4ee02e59..8fd9aaacd45 100644 --- a/packages/core/src/tasks/commands/handlers/automation-task.sync.handler.ts +++ b/packages/core/src/tasks/commands/handlers/automation-task.sync.handler.ts @@ -175,8 +175,11 @@ export class AutomationTaskSyncHandler implements ICommandHandler { try { + // Find task relations + const relations = this.typeOrmTaskRepository.metadata.relations.map((relation) => relation.propertyName); + // Find the existing task by its ID - const existingTask = await this._taskService.findOneByIdString(id); + const existingTask = await this._taskService.findOneByIdString(id, { relations }); if (!existingTask) { return; } diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index 61cca691699..576518b8590 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -61,7 +61,11 @@ export class TaskService extends TenantAwareCrudService { const tenantId = RequestContext.currentTenantId() || input.tenantId; const userId = RequestContext.currentUserId(); const { organizationSprintId } = input; - const task = await this.findOneByIdString(id); + + // Find task relations + const relations = this.typeOrmTaskRepository.metadata.relations.map((relation) => relation.propertyName); + + const task = await this.findOneByIdString(id, { relations }); if (input.projectId && input.projectId !== task.projectId) { const { organizationId, projectId } = task; From cee867be92f1da56f2e05173f596df8ee14ac4f7 Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Tue, 29 Oct 2024 20:12:03 +0200 Subject: [PATCH 08/30] Create organization project service --- .../organization-projects-module.service.ts | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 packages/ui-core/core/src/lib/services/organization/organization-projects-module.service.ts diff --git a/packages/ui-core/core/src/lib/services/organization/organization-projects-module.service.ts b/packages/ui-core/core/src/lib/services/organization/organization-projects-module.service.ts new file mode 100644 index 00000000000..61613f5b514 --- /dev/null +++ b/packages/ui-core/core/src/lib/services/organization/organization-projects-module.service.ts @@ -0,0 +1,97 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { + IOrganizationProjectModule, + IPagination, + ID, + IOrganizationProjectModuleCreateInput, + IOrganizationProjectModuleUpdateInput +} from '@gauzy/contracts'; +import { API_PREFIX } from '@gauzy/ui-core/common'; + +@Injectable({ + providedIn: 'root' +}) +export class OrganizationProjectModuleService { + private readonly API_URL = `${API_PREFIX}/organization-project-module`; + + constructor(private readonly http: HttpClient) {} + + /** + * Create a new project module with the provided data. + * @param {IOrganizationProjectModuleCreateInput} data - The data for creating the new project module. + * @returns An Observable that emits the newly created project module. + */ + create(data: IOrganizationProjectModuleCreateInput): Observable { + return this.http.post(this.API_URL, data); + } + + /** + * Update an existing project module identified by its ID with the new data. + * @param {ID} id - The unique identifier of the project module to update. + * @param {IOrganizationProjectModuleUpdateInput} data - The new data for updating the project module. + * @returns An Observable that emits the updated project module or result. + */ + update(id: ID, data: IOrganizationProjectModuleUpdateInput): Observable { + return this.http.put(`${this.API_URL}/${id}`, data); + } + + /** + * Find project modules for an employee based on pagination parameters. + * @param params - The pagination parameters for filtering employee project modules. + * @returns An Observable that emits the paginated list of employee project modules. + */ + getEmployeeProjectModules(params: HttpParams): Observable> { + return this.http.get>(`${this.API_URL}/employee`, { params }); + } + + /** + * Retrieve project modules associated with a team using pagination parameters. + * @param params - The pagination parameters for filtering team project modules. + * @returns An Observable that emits the paginated list of team project modules. + */ + findTeamProjectModules(params: HttpParams): Observable> { + return this.http.get>(`${this.API_URL}/team`, { params }); + } + + /** + * Retrieve project modules associated with a specific employee. + * @param employeeId - The unique identifier of the employee. + * @param params - Additional query parameters for filtering. + * @returns An Observable that emits the paginated list of project modules for the specified employee. + */ + findByEmployee(employeeId: ID, params: HttpParams): Observable> { + return this.http.get>(`${this.API_URL}/employee/${employeeId}`, { + params + }); + } + + /** + * Retrieve all project modules with pagination parameters. + * @param params - The pagination parameters for filtering all project modules. + * @returns An Observable that emits the paginated list of project modules. + */ + findAll(params: HttpParams): Observable> { + return this.http.get>(this.API_URL, { params }); + } + + /** + * Find a specific project module by its unique identifier. + * @param id - The unique identifier of the project module. + * @param params - Additional query parameters if required. + * @returns An Observable that emits the found project module. + */ + findById(id: ID, params: HttpParams): Observable { + return this.http.get(`${this.API_URL}/${id}`, { params }); + } + + /** + * Delete an existing project module by its unique identifier. + * @param id - The unique identifier of the project module to delete. + * @returns An Observable that emits the result of the delete operation. + */ + delete(id: ID): Observable { + return this.http.delete(`${this.API_URL}/${id}`); + } +} From 0af8e058b67865400770732f1116ca98f69f89c9 Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Tue, 29 Oct 2024 20:49:28 +0200 Subject: [PATCH 09/30] Fix deep scan --- .../edit-user-organizations.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gauzy/src/app/pages/users/edit-user-profile/edit-user-organizations/edit-user-organizations.component.ts b/apps/gauzy/src/app/pages/users/edit-user-profile/edit-user-organizations/edit-user-organizations.component.ts index 0f8bdd1c6cf..b7401a6bbe8 100644 --- a/apps/gauzy/src/app/pages/users/edit-user-profile/edit-user-organizations/edit-user-organizations.component.ts +++ b/apps/gauzy/src/app/pages/users/edit-user-profile/edit-user-organizations/edit-user-organizations.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; -import { IOrganization, IUserOrganizationCreateInput, RolesEnum } from '@gauzy/contracts'; +import { IOrganization, IUserOrganizationCreateInput } from '@gauzy/contracts'; import { filter, tap, debounceTime } from 'rxjs/operators'; import { Subject, firstValueFrom } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; From dc61eeae75b98461ed8246280a51cda75b1494cf Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Tue, 29 Oct 2024 21:12:31 +0200 Subject: [PATCH 10/30] integration of coderabitai suggestions --- .../edit-user-organizations.component.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/gauzy/src/app/pages/users/edit-user-profile/edit-user-organizations/edit-user-organizations.component.ts b/apps/gauzy/src/app/pages/users/edit-user-profile/edit-user-organizations/edit-user-organizations.component.ts index b7401a6bbe8..b96af716b77 100644 --- a/apps/gauzy/src/app/pages/users/edit-user-profile/edit-user-organizations/edit-user-organizations.component.ts +++ b/apps/gauzy/src/app/pages/users/edit-user-profile/edit-user-organizations/edit-user-organizations.component.ts @@ -84,7 +84,6 @@ export class EditUserOrganizationsComponent extends TranslationBaseComponent imp async remove(id: string) { const { tenantId } = this.store.user; - const user = await this.usersService.getUserById(this.selectedUserId); const { items } = await this.userOrganizationsService.getAll(['user', 'user.role'], { tenantId, userId: this.selectedUserId @@ -93,14 +92,17 @@ export class EditUserOrganizationsComponent extends TranslationBaseComponent imp let counter = 0; let userName: string; - this.userToRemove = items.find((orgUser) => orgUser.organizationId === id); + this.userToRemove = items.find((orgUser) => orgUser.organizationId === id && orgUser.isActive); - if (this.userToRemove) { - userName = this.userToRemove.user.firstName + ' ' + this.userToRemove.user.lastName; - this.orgUserId = this.userToRemove.id; - counter = items.filter((orgUser) => orgUser.user.id === user.id && orgUser.isActive).length; + if (!this.userToRemove?.user) { + this.toastrService.danger('User organization record not found'); + return; } + userName = [this.userToRemove.user.firstName, this.userToRemove.user.lastName].filter(Boolean).join(' '); + this.orgUserId = this.userToRemove.id; + counter = items.filter((orgUser) => orgUser.isActive).length; + if (counter - 1 < 1) { this.dialogService .open(DeleteConfirmationComponent, { From f3f42b714e953cbd1a1ba07123793dc32a557127 Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Wed, 30 Oct 2024 09:21:32 +0200 Subject: [PATCH 11/30] Extend organization project module service --- .../organization-projects-module.service.ts | 52 +++---------------- 1 file changed, 6 insertions(+), 46 deletions(-) diff --git a/packages/ui-core/core/src/lib/services/organization/organization-projects-module.service.ts b/packages/ui-core/core/src/lib/services/organization/organization-projects-module.service.ts index 61613f5b514..1d13896ccac 100644 --- a/packages/ui-core/core/src/lib/services/organization/organization-projects-module.service.ts +++ b/packages/ui-core/core/src/lib/services/organization/organization-projects-module.service.ts @@ -1,40 +1,18 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { - IOrganizationProjectModule, - IPagination, - ID, - IOrganizationProjectModuleCreateInput, - IOrganizationProjectModuleUpdateInput -} from '@gauzy/contracts'; +import { IOrganizationProjectModule, IPagination, ID } from '@gauzy/contracts'; import { API_PREFIX } from '@gauzy/ui-core/common'; +import { CrudService } from '../crud'; @Injectable({ providedIn: 'root' }) -export class OrganizationProjectModuleService { - private readonly API_URL = `${API_PREFIX}/organization-project-module`; +export class OrganizationProjectModuleService extends CrudService { + private static readonly API_URL = `${API_PREFIX}/organization-project-module`; - constructor(private readonly http: HttpClient) {} - - /** - * Create a new project module with the provided data. - * @param {IOrganizationProjectModuleCreateInput} data - The data for creating the new project module. - * @returns An Observable that emits the newly created project module. - */ - create(data: IOrganizationProjectModuleCreateInput): Observable { - return this.http.post(this.API_URL, data); - } - - /** - * Update an existing project module identified by its ID with the new data. - * @param {ID} id - The unique identifier of the project module to update. - * @param {IOrganizationProjectModuleUpdateInput} data - The new data for updating the project module. - * @returns An Observable that emits the updated project module or result. - */ - update(id: ID, data: IOrganizationProjectModuleUpdateInput): Observable { - return this.http.put(`${this.API_URL}/${id}`, data); + constructor(http: HttpClient) { + super(http, OrganizationProjectModuleService.API_URL); } /** @@ -67,15 +45,6 @@ export class OrganizationProjectModuleService { }); } - /** - * Retrieve all project modules with pagination parameters. - * @param params - The pagination parameters for filtering all project modules. - * @returns An Observable that emits the paginated list of project modules. - */ - findAll(params: HttpParams): Observable> { - return this.http.get>(this.API_URL, { params }); - } - /** * Find a specific project module by its unique identifier. * @param id - The unique identifier of the project module. @@ -85,13 +54,4 @@ export class OrganizationProjectModuleService { findById(id: ID, params: HttpParams): Observable { return this.http.get(`${this.API_URL}/${id}`, { params }); } - - /** - * Delete an existing project module by its unique identifier. - * @param id - The unique identifier of the project module to delete. - * @returns An Observable that emits the result of the delete operation. - */ - delete(id: ID): Observable { - return this.http.delete(`${this.API_URL}/${id}`); - } } From bc0d6e3f8682d2118637ba9daecfe4447366a028 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Wed, 30 Oct 2024 13:28:12 +0530 Subject: [PATCH 12/30] fix: updated employee other settings (#8511) --- .../edit-employee-other-settings.component.ts | 61 +++++++++---------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.ts b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.ts index fe79b27c4a6..e3d2655b1af 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.ts +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.ts @@ -74,68 +74,63 @@ export class EditEmployeeOtherSettingsComponent implements OnInit, OnDestroy { } /** + * Patches the form with employee data or default values if data is unavailable. * - * @param employee - * @returns + * @param {IEmployee} employee - The employee object containing user data. + * @returns {void} */ - private _patchFormValue(employee: IEmployee) { - if (!employee) { - return; - } - const { user } = employee; + private _patchFormValue(employee: IEmployee): void { + if (!employee) return; + + const { user, upworkId, linkedInId, allowManualTime, allowDeleteTime, allowModifyTime, allowScreenshotCapture } = employee; this.form.patchValue({ - timeZone: user.timeZone || moment.tz.guess(), // set current timezone, if employee don't have any timezone - timeFormat: user.timeFormat, - upworkId: employee.upworkId, - linkedInId: employee.linkedInId, - allowManualTime: employee.allowManualTime, - allowDeleteTime: employee.allowDeleteTime, - allowModifyTime: employee.allowModifyTime, - allowScreenshotCapture: employee.allowScreenshotCapture + timeZone: user?.timeZone ?? moment.tz.guess(), + timeFormat: user?.timeFormat, + upworkId, + linkedInId, + allowManualTime, + allowDeleteTime, + allowModifyTime, + allowScreenshotCapture }); this.form.updateValueAndValidity(); } /** + * Handles the form submission, updating employee and user settings if valid. * - * @param form - * @returns + * @param {NgForm} form - The form reference for submission. + * @returns {void} */ - onSubmit(form: NgForm) { - if (form.invalid) { - return; - } + onSubmit(form: NgForm): void { + if (form.invalid) return; + const { organizationId, tenantId } = this.selectedEmployee; const { timeZone, timeFormat, upworkId, linkedInId, - allowScreenshotCapture, allowManualTime, + allowDeleteTime, allowModifyTime, - allowDeleteTime + allowScreenshotCapture } = this.form.value; - /** Update user fields */ - this.employeeStore.userForm = { - timeZone, - timeFormat - }; - - /** Update employee fields */ - this.employeeStore.employeeForm = { + this.employeeStore.updateUserForm({ timeZone, timeFormat }); + this.employeeStore.updateEmployeeForm({ upworkId, linkedInId, organizationId, tenantId, allowManualTime, - allowModifyTime, allowDeleteTime, + allowModifyTime, allowScreenshotCapture - }; + }); } + /** * */ From 2fd3f1fa7561843887db09f3dfb7351a707817f7 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 31 Oct 2024 08:43:54 +0200 Subject: [PATCH 13/30] feat: create a helper function to map issue relation description --- packages/contracts/src/base-entity.model.ts | 1 + .../src/activity-log/activity-log.service.ts | 2 +- .../linked-issue/task-linked-issue.helper.ts | 21 +++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/tasks/linked-issue/task-linked-issue.helper.ts diff --git a/packages/contracts/src/base-entity.model.ts b/packages/contracts/src/base-entity.model.ts index 81951b07154..86f41428ca2 100644 --- a/packages/contracts/src/base-entity.model.ts +++ b/packages/contracts/src/base-entity.model.ts @@ -87,5 +87,6 @@ export enum BaseEntityEnum { OrganizationVendor = 'OrganizationVendor', Task = 'Task', TaskView = 'TaskView', + TaskLinkedIssue = 'TaskLinkedIssue', User = 'User' } diff --git a/packages/core/src/activity-log/activity-log.service.ts b/packages/core/src/activity-log/activity-log.service.ts index fa18e7334ac..1019cc68d37 100644 --- a/packages/core/src/activity-log/activity-log.service.ts +++ b/packages/core/src/activity-log/activity-log.service.ts @@ -122,7 +122,7 @@ export class ActivityLogService extends TenantAwareCrudService { /** * @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 diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.helper.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.helper.ts new file mode 100644 index 00000000000..f8843b9871b --- /dev/null +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.helper.ts @@ -0,0 +1,21 @@ +import { TaskRelatedIssuesRelationEnum } from '@gauzy/contracts'; + +/** + * Maps a task's related issue relation enum to a corresponding string description. + * + * @param {TaskRelatedIssuesRelationEnum} relation - The relation type from the enum `TaskRelatedIssuesRelationEnum`. + * @returns {string} The corresponding string description for the given relation type. + */ +export function taskRelatedIssueRelationMap(relation: TaskRelatedIssuesRelationEnum): string { + const issueRelationMap: { [key in TaskRelatedIssuesRelationEnum]: string } = { + [TaskRelatedIssuesRelationEnum.BLOCKS]: 'Blocks', + [TaskRelatedIssuesRelationEnum.CLONES]: 'Clones', + [TaskRelatedIssuesRelationEnum.DUPLICATES]: 'Duplicates', + [TaskRelatedIssuesRelationEnum.IS_BLOCKED_BY]: 'Is Blocked By', + [TaskRelatedIssuesRelationEnum.IS_CLONED_BY]: 'Is cloned By', + [TaskRelatedIssuesRelationEnum.IS_DUPLICATED_BY]: 'Is Duplicated By', + [TaskRelatedIssuesRelationEnum.RELATES_TO]: 'Relates To' + }; + + return issueRelationMap[relation]; +} From 38de69c73ed7607622e96f24152419a451b3a903 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 31 Oct 2024 08:58:08 +0200 Subject: [PATCH 14/30] fix: improve task linked issue creation supporting activity log --- .../contracts/src/task-linked-issue.model.ts | 16 +++--- .../linked-issue/dto/task-linked-issue.dto.ts | 22 +++----- .../task-linked-issue.controller.ts | 4 +- .../linked-issue/task-linked-issue.entity.ts | 15 ++---- .../linked-issue/task-linked-issue.service.ts | 53 +++++++++++++++++-- 5 files changed, 69 insertions(+), 41 deletions(-) diff --git a/packages/contracts/src/task-linked-issue.model.ts b/packages/contracts/src/task-linked-issue.model.ts index a7b0f71d6dc..0cac874aaa8 100644 --- a/packages/contracts/src/task-linked-issue.model.ts +++ b/packages/contracts/src/task-linked-issue.model.ts @@ -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 { @@ -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 { - id?: string; +export interface ITaskLinkedIssueUpdateInput extends Partial { + id?: ID; } export interface ILinkedIssueFindInput diff --git a/packages/core/src/tasks/linked-issue/dto/task-linked-issue.dto.ts b/packages/core/src/tasks/linked-issue/dto/task-linked-issue.dto.ts index 48eb54fd381..e43b0e0262f 100644 --- a/packages/core/src/tasks/linked-issue/dto/task-linked-issue.dto.ts +++ b/packages/core/src/tasks/linked-issue/dto/task-linked-issue.dto.ts @@ -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 {} diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts index ed6b90881e7..7113aa0facd 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, HttpCode, HttpStatus, Param, Post, Put, UseGuards } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ITaskLinkedIssue, PermissionsEnum } from '@gauzy/contracts'; +import { ID, ITaskLinkedIssue, PermissionsEnum } from '@gauzy/contracts'; import { PermissionGuard, TenantPermissionGuard } from '../../shared/guards'; import { UUIDValidationPipe, UseValidationPipe } from '../../shared/pipes'; import { Permissions } from '../../shared/decorators'; @@ -44,7 +44,7 @@ export class TaskLinkedIssueController extends CrudController { @Put(':id') @UseValidationPipe({ whitelist: true }) async update( - @Param('id', UUIDValidationPipe) id: ITaskLinkedIssue['id'], + @Param('id', UUIDValidationPipe) id: ID, @Body() entity: UpdateTaskLinkedIssueDTO ): Promise { return await this.taskLinkedIssueService.create({ diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.entity.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.entity.ts index 3ccdd23db02..c971d2b57cb 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.entity.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.entity.ts @@ -1,14 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { - JoinColumn, - RelationId, -} from 'typeorm'; +import { JoinColumn, RelationId } from 'typeorm'; import { IsEnum, IsUUID } from 'class-validator'; -import { - ITask, - ITaskLinkedIssue, - TaskRelatedIssuesRelationEnum, -} from '@gauzy/contracts'; +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'; @@ -36,7 +29,7 @@ export class TaskLinkedIssue extends TenantOrganizationBaseEntity implements ITa @RelationId((it: TaskLinkedIssue) => it.taskFrom) @ColumnIndex() @MultiORMColumn({ relationId: true }) - taskFromId: ITask['id']; + taskFromId: ID; /** * Task Linked Issues @@ -51,5 +44,5 @@ export class TaskLinkedIssue extends TenantOrganizationBaseEntity implements ITa @RelationId((it: TaskLinkedIssue) => it.taskTo) @ColumnIndex() @MultiORMColumn({ relationId: true }) - taskToId: ITask['id']; + taskToId: ID; } diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts index b713da8f458..f1edf0dfbf8 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts @@ -1,9 +1,19 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { TaskLinkedIssue } from './task-linked-issue.entity'; +import { + ActionTypeEnum, + ActorTypeEnum, + BaseEntityEnum, + ITaskLinkedIssue, + ITaskLinkedIssueCreateInput +} from '@gauzy/contracts'; import { TenantAwareCrudService } from '../../core/crud'; +import { RequestContext } from '../../core/context'; +import { ActivityLogService } from '../../activity-log/activity-log.service'; +import { TaskLinkedIssue } from './task-linked-issue.entity'; import { MikroOrmTaskLinkedIssueRepository } from './repository/mikro-orm-linked-issue.repository'; import { TypeOrmTaskLinkedIssueRepository } from './repository/type-orm-linked-issue.repository'; +import { taskRelatedIssueRelationMap } from './task-linked-issue.helper'; @Injectable() export class TaskLinkedIssueService extends TenantAwareCrudService { @@ -11,8 +21,45 @@ export class TaskLinkedIssueService extends TenantAwareCrudService} The created task linked issue. + * @throws {HttpException} Throws a Bad Request exception if task creation fails. + * + */ + async create(entity: ITaskLinkedIssueCreateInput): Promise { + const tenantId = RequestContext.currentTenantId() || entity.tenantId; + const { organizationId } = entity; + + try { + const taskLinkedIssue = await super.create({ ...entity, tenantId }); + + // Generate the activity log + this.activityLogService.logActivity( + BaseEntityEnum.TaskLinkedIssue, + ActionTypeEnum.Created, + ActorTypeEnum.User, + taskLinkedIssue.id, + taskRelatedIssueRelationMap(taskLinkedIssue.action), + taskLinkedIssue, + organizationId, + tenantId + ); + + // Return the created task linked issue + return taskLinkedIssue; + } catch (error) { + // Handle errors and return an appropriate error response + throw new HttpException(`Failed to create task linked issue : ${error.message}`, HttpStatus.BAD_REQUEST); + } + } } From 06b9d20489b6349768977e73b94f26522ca61408 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 31 Oct 2024 09:20:08 +0200 Subject: [PATCH 15/30] fix: issue updating sprint --- .../linked-issue/task-linked-issue.service.ts | 52 ++++++++++++++++++- packages/core/src/tasks/task.service.ts | 2 +- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts index f1edf0dfbf8..e557c11f9c5 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts @@ -1,11 +1,13 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ActionTypeEnum, ActorTypeEnum, BaseEntityEnum, + ID, ITaskLinkedIssue, - ITaskLinkedIssueCreateInput + ITaskLinkedIssueCreateInput, + ITaskLinkedIssueUpdateInput } from '@gauzy/contracts'; import { TenantAwareCrudService } from '../../core/crud'; import { RequestContext } from '../../core/context'; @@ -62,4 +64,50 @@ export class TaskLinkedIssueService extends TenantAwareCrudService} The updated task linked issue. + * @throws {HttpException} Throws a Bad Request exception if the update fails. + * @throws {NotFoundException} Throws a Not Found exception if the task linked issue does not exist. + * + */ + async update(id: ID, input: ITaskLinkedIssueUpdateInput): Promise { + const tenantId = RequestContext.currentTenantId() || input.tenantId; + + try { + // Retrieve existing task linked issue + const existingTaskLinkedIssue = await this.findOneByIdString(id); + + if (!existingTaskLinkedIssue) { + throw new NotFoundException('View not found'); + } + + const updatedTaskLinkedIssue = await super.create({ ...input, tenantId, id }); + + // Generate the activity log + const { organizationId } = updatedTaskLinkedIssue; + this.activityLogService.logActivity( + BaseEntityEnum.TaskLinkedIssue, + ActionTypeEnum.Updated, + ActorTypeEnum.User, + updatedTaskLinkedIssue.id, + taskRelatedIssueRelationMap(updatedTaskLinkedIssue.action), + updatedTaskLinkedIssue, + organizationId, + tenantId, + existingTaskLinkedIssue, + input + ); + + // return the updated task linked issue + return updatedTaskLinkedIssue; + } catch (error) { + // Handle errors and return an appropriate error response + throw new HttpException(`Failed to update task linked issue: ${error.message}`, HttpStatus.BAD_REQUEST); + } + } } diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index 61cca691699..292208348c9 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -89,7 +89,7 @@ export class TaskService extends TenantAwareCrudService { // Register Task Sprint moving history if (organizationSprintId && organizationSprintId !== task.organizationSprintId) { await this.typeOrmOrganizationSprintTaskHistoryRepository.save({ - fromSprintId: task.organizationSprintId, + fromSprintId: task.organizationSprintId || organizationSprintId, // Use incoming sprint ID if the task's organizationSprintId was priviously null or undefined toSprintId: organizationSprintId, taskId: updatedTask.id, movedById: userId, From 7196224a3399105d7842d6473ac37b91e05f4dc9 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 31 Oct 2024 09:27:48 +0200 Subject: [PATCH 16/30] feat: add task linked issue commands --- .../src/tasks/linked-issue/commands/handlers/index.ts | 0 .../commands/handlers/task-linked-issue-create.handler.ts | 0 .../commands/handlers/task-linked-issue-update.handler.ts | 0 packages/core/src/tasks/linked-issue/commands/index.ts | 2 ++ .../commands/task-linked-issue-create.command.ts | 8 ++++++++ .../commands/task-linked-issue-update.command.ts | 8 ++++++++ 6 files changed, 18 insertions(+) create mode 100644 packages/core/src/tasks/linked-issue/commands/handlers/index.ts create mode 100644 packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-create.handler.ts create mode 100644 packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-update.handler.ts create mode 100644 packages/core/src/tasks/linked-issue/commands/index.ts create mode 100644 packages/core/src/tasks/linked-issue/commands/task-linked-issue-create.command.ts create mode 100644 packages/core/src/tasks/linked-issue/commands/task-linked-issue-update.command.ts diff --git a/packages/core/src/tasks/linked-issue/commands/handlers/index.ts b/packages/core/src/tasks/linked-issue/commands/handlers/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-create.handler.ts b/packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-create.handler.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-update.handler.ts b/packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-update.handler.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/tasks/linked-issue/commands/index.ts b/packages/core/src/tasks/linked-issue/commands/index.ts new file mode 100644 index 00000000000..67caf1505cd --- /dev/null +++ b/packages/core/src/tasks/linked-issue/commands/index.ts @@ -0,0 +1,2 @@ +export * from './task-linked-issue-create.command'; +export * from './task-linked-issue-update.command'; diff --git a/packages/core/src/tasks/linked-issue/commands/task-linked-issue-create.command.ts b/packages/core/src/tasks/linked-issue/commands/task-linked-issue-create.command.ts new file mode 100644 index 00000000000..2b02682c765 --- /dev/null +++ b/packages/core/src/tasks/linked-issue/commands/task-linked-issue-create.command.ts @@ -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) {} +} diff --git a/packages/core/src/tasks/linked-issue/commands/task-linked-issue-update.command.ts b/packages/core/src/tasks/linked-issue/commands/task-linked-issue-update.command.ts new file mode 100644 index 00000000000..732cf48b4f4 --- /dev/null +++ b/packages/core/src/tasks/linked-issue/commands/task-linked-issue-update.command.ts @@ -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) {} +} From 707d9a61481db5416c8f634506f086ffadc9f6a4 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 31 Oct 2024 09:37:53 +0200 Subject: [PATCH 17/30] feat: add task linked issue command handlers --- .../tasks/linked-issue/commands/handlers/index.ts | 4 ++++ .../handlers/task-linked-issue-create.handler.ts | 15 +++++++++++++++ .../handlers/task-linked-issue-update.handler.ts | 15 +++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/packages/core/src/tasks/linked-issue/commands/handlers/index.ts b/packages/core/src/tasks/linked-issue/commands/handlers/index.ts index e69de29bb2d..d78cf9b943f 100644 --- a/packages/core/src/tasks/linked-issue/commands/handlers/index.ts +++ b/packages/core/src/tasks/linked-issue/commands/handlers/index.ts @@ -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]; diff --git a/packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-create.handler.ts b/packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-create.handler.ts index e69de29bb2d..a5bb065c6b1 100644 --- a/packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-create.handler.ts +++ b/packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-create.handler.ts @@ -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 { + constructor(private readonly taskLinkedIssueService: TaskLinkedIssueService) {} + + public async execute(command: TaskLinkedIssueCreateCommand): Promise { + const { input } = command; + + return await this.taskLinkedIssueService.create(input); + } +} diff --git a/packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-update.handler.ts b/packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-update.handler.ts index e69de29bb2d..761631b5eac 100644 --- a/packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-update.handler.ts +++ b/packages/core/src/tasks/linked-issue/commands/handlers/task-linked-issue-update.handler.ts @@ -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 { + constructor(private readonly taskLinkedIssueService: TaskLinkedIssueService) {} + + public async execute(command: TaskLinkedIssueUpdateCommand): Promise { + const { id, input } = command; + + return await this.taskLinkedIssueService.update(id, input); + } +} From a313013a0195411e056779b73a2638b817214632 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 31 Oct 2024 09:45:32 +0200 Subject: [PATCH 18/30] fix: improve task linked issue using commands --- .../task-linked-issue.controller.ts | 46 ++++++++++++++----- .../linked-issue/task-linked-issue.module.ts | 5 +- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts index 7113aa0facd..b5d52294854 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts @@ -1,5 +1,6 @@ import { Body, Controller, HttpCode, HttpStatus, Param, Post, Put, UseGuards } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { CommandBus } from '@nestjs/cqrs'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ID, ITaskLinkedIssue, PermissionsEnum } from '@gauzy/contracts'; import { PermissionGuard, TenantPermissionGuard } from '../../shared/guards'; import { UUIDValidationPipe, UseValidationPipe } from '../../shared/pipes'; @@ -8,37 +9,63 @@ 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) @Controller() export class TaskLinkedIssueController extends CrudController { - constructor(protected readonly taskLinkedIssueService: TaskLinkedIssueService) { + constructor( + protected readonly taskLinkedIssueService: TaskLinkedIssueService, + private readonly commandBus: CommandBus + ) { super(taskLinkedIssueService); } /** * Create new Linked Issue * - * @param entity - * @returns + * @param entity - The input data for creating a 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 { - return await this.taskLinkedIssueService.create(entity); + return await this.commandBus.execute(new TaskLinkedIssueCreateCommand(entity)); } /** * Update existing Linked Issue * - * @param id - * @param entity + * @param id - The ID of the task linked issue to update. + * @param entity - The input data for updating the task linked issue. * @returns */ + @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') @@ -47,9 +74,6 @@ export class TaskLinkedIssueController extends CrudController { @Param('id', UUIDValidationPipe) id: ID, @Body() entity: UpdateTaskLinkedIssueDTO ): Promise { - return await this.taskLinkedIssueService.create({ - ...entity, - id - }); + return await this.commandBus.execute(new TaskLinkedIssueUpdateCommand(id, entity)); } } diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.module.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.module.ts index cf3098adf43..583c0ed116b 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.module.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.module.ts @@ -5,6 +5,7 @@ import { RouterModule } from '@nestjs/core'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { RolePermissionModule } from '../../role-permission/role-permission.module'; import { TaskLinkedIssue } from './task-linked-issue.entity'; +import { CommandHandlers } from './commands/handlers'; import { TaskLinkedIssueController } from './task-linked-issue.controller'; import { TaskLinkedIssueService } from './task-linked-issue.service'; @@ -17,7 +18,7 @@ import { TaskLinkedIssueService } from './task-linked-issue.service'; CqrsModule ], controllers: [TaskLinkedIssueController], - providers: [TaskLinkedIssueService], + providers: [TaskLinkedIssueService, ...CommandHandlers], exports: [TaskLinkedIssueService] }) -export class TaskLinkedIssueModule { } +export class TaskLinkedIssueModule {} From 8cd2630e3b0c10ea29f7f5678b207335397e6678 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 31 Oct 2024 10:17:56 +0200 Subject: [PATCH 19/30] feat: task-linked issue delete log --- .../task-linked-issue.controller.ts | 18 +++++++- .../linked-issue/task-linked-issue.service.ts | 42 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts index b5d52294854..21da1230230 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts @@ -1,6 +1,7 @@ -import { Body, Controller, HttpCode, HttpStatus, Param, Post, Put, UseGuards } from '@nestjs/common'; +import { Body, Controller, Delete, HttpCode, HttpStatus, Param, Post, Put, UseGuards } from '@nestjs/common'; import { CommandBus } from '@nestjs/cqrs'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { DeleteResult } from 'typeorm'; import { ID, ITaskLinkedIssue, PermissionsEnum } from '@gauzy/contracts'; import { PermissionGuard, TenantPermissionGuard } from '../../shared/guards'; import { UUIDValidationPipe, UseValidationPipe } from '../../shared/pipes'; @@ -76,4 +77,19 @@ export class TaskLinkedIssueController extends CrudController { ): Promise { return await this.commandBus.execute(new TaskLinkedIssueUpdateCommand(id, entity)); } + + @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) + @Delete('/:id') + async delete(@Param('id', UUIDValidationPipe) id: ID): Promise { + return await this.taskLinkedIssueService.delete(id); + } } diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts index e557c11f9c5..ae24ad538b2 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts @@ -1,5 +1,6 @@ import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { DeleteResult, FindOneOptions } from 'typeorm'; import { ActionTypeEnum, ActorTypeEnum, @@ -110,4 +111,45 @@ export class TaskLinkedIssueService extends TenantAwareCrudService} [options] - Optional query options to find the task linked issue before deletion. + * @returns {Promise} The result of the deletion operation. + * @throws {HttpException} Throws a Bad Request exception if the deletion fails. + * @throws {NotFoundException} Throws a Not Found exception if the task linked issue does not exist. + * + */ + async delete(id: ID, options?: FindOneOptions): Promise { + const tenantId = RequestContext.currentTenantId(); + try { + // Retrieve existing task linked issue + const existingTaskLinkedIssue = await this.findOneByIdString(id); + + if (!existingTaskLinkedIssue) { + throw new NotFoundException('View not found'); + } + + // Generate deleted activity log + const { organizationId } = existingTaskLinkedIssue; + this.activityLogService.logActivity( + BaseEntityEnum.TaskLinkedIssue, + ActionTypeEnum.Deleted, + ActorTypeEnum.User, + id, + taskRelatedIssueRelationMap(existingTaskLinkedIssue.action), + existingTaskLinkedIssue, + organizationId, + tenantId + ); + + // + return await super.delete(id, options); + } catch (error) { + // Handle errors and return an appropriate error response + throw new HttpException(`Failed to delete task linked issue: ${error.message}`, HttpStatus.BAD_REQUEST); + } + } } From b069f28ade026ea5cfbcaaf1a7a5dd85e76aa892 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 31 Oct 2024 10:23:05 +0200 Subject: [PATCH 20/30] fix(typos): cspell error --- packages/core/src/tasks/task.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index 292208348c9..f6aed374fb4 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -89,7 +89,7 @@ export class TaskService extends TenantAwareCrudService { // Register Task Sprint moving history if (organizationSprintId && organizationSprintId !== task.organizationSprintId) { await this.typeOrmOrganizationSprintTaskHistoryRepository.save({ - fromSprintId: task.organizationSprintId || organizationSprintId, // Use incoming sprint ID if the task's organizationSprintId was priviously null or undefined + fromSprintId: task.organizationSprintId || organizationSprintId, // Use incoming sprint ID if the task's organizationSprintId was previously null or undefined toSprintId: organizationSprintId, taskId: updatedTask.id, movedById: userId, From 352c847cb8cbd7000f1cc5007bb183cab229be36 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 31 Oct 2024 10:42:27 +0200 Subject: [PATCH 21/30] fix: improve code by rabbit suggestions --- .../src/tasks/linked-issue/task-linked-issue.helper.ts | 8 ++++++-- .../src/tasks/linked-issue/task-linked-issue.service.ts | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.helper.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.helper.ts index f8843b9871b..a362dcf90be 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.helper.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.helper.ts @@ -12,10 +12,14 @@ export function taskRelatedIssueRelationMap(relation: TaskRelatedIssuesRelationE [TaskRelatedIssuesRelationEnum.CLONES]: 'Clones', [TaskRelatedIssuesRelationEnum.DUPLICATES]: 'Duplicates', [TaskRelatedIssuesRelationEnum.IS_BLOCKED_BY]: 'Is Blocked By', - [TaskRelatedIssuesRelationEnum.IS_CLONED_BY]: 'Is cloned By', + [TaskRelatedIssuesRelationEnum.IS_CLONED_BY]: 'Is Cloned By', [TaskRelatedIssuesRelationEnum.IS_DUPLICATED_BY]: 'Is Duplicated By', [TaskRelatedIssuesRelationEnum.RELATES_TO]: 'Relates To' }; - return issueRelationMap[relation]; + const issueRelation = issueRelationMap[relation]; + if (!issueRelation) { + throw new Error(`Unsupported relation type: ${relation}`); + } + return issueRelation; } diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts index ae24ad538b2..39d442140a0 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts @@ -84,7 +84,7 @@ export class TaskLinkedIssueService extends TenantAwareCrudService Date: Thu, 31 Oct 2024 11:53:07 +0200 Subject: [PATCH 22/30] feat: only reset icon when remote timer is not running, and added ngIf condition to only render gauzy-time-tracker-status when isRemoteTimer is true. --- .../time-tracker-status/time-tracker-status.service.ts | 2 +- .../src/lib/time-tracker/time-tracker.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker-status/time-tracker-status.service.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker-status/time-tracker-status.service.ts index 6fa1c3002b2..f0ac8e80573 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker-status/time-tracker-status.service.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker-status/time-tracker-status.service.ts @@ -58,7 +58,7 @@ export class TimeTrackerStatusService { duration: status.duration }); this._icon$.next(TimerIconFactory.create(remoteTimer.source)); - if (!remoteTimer.running || !remoteTimer.isExternalSource) this._icon$.next(null); + if (!remoteTimer.running) this._icon$.next(null); this._external$.next(remoteTimer); }), repeat({ diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html index 516a3566736..b3f761adaa3 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html @@ -54,7 +54,7 @@ - +
From c19b0be8585c4208d230f0f388cc554d8b0c77aa Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 31 Oct 2024 13:01:00 +0200 Subject: [PATCH 23/30] fix: task linked issue find all using pagination params --- .../task-linked-issue.controller.ts | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts index 21da1230230..d475111973c 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts @@ -1,12 +1,24 @@ -import { Body, Controller, Delete, HttpCode, HttpStatus, Param, Post, Put, UseGuards } from '@nestjs/common'; +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, ITaskLinkedIssue, PermissionsEnum } from '@gauzy/contracts'; +import { ID, IPagination, ITaskLinkedIssue, PermissionsEnum } from '@gauzy/contracts'; import { PermissionGuard, TenantPermissionGuard } from '../../shared/guards'; import { UUIDValidationPipe, UseValidationPipe } from '../../shared/pipes'; import { Permissions } from '../../shared/decorators'; -import { CrudController } from '../../core/crud'; +import { CrudController, PaginationParams } from '../../core/crud'; import { TaskLinkedIssue } from './task-linked-issue.entity'; import { TaskLinkedIssueService } from './task-linked-issue.service'; import { CreateTaskLinkedIssueDTO, UpdateTaskLinkedIssueDTO } from './dto'; @@ -24,6 +36,24 @@ export class TaskLinkedIssueController extends CrudController { super(taskLinkedIssueService); } + @ApiOperation({ + summary: 'Find all' + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Found task linked issues', + type: TaskLinkedIssue + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Record not found' + }) + @Get() + @UseValidationPipe() + async findAll(@Query() params: PaginationParams): Promise> { + return await this.taskLinkedIssueService.findAll(params); + } + /** * Create new Linked Issue * From fdc32ec40c19c8f563020590015a9573370b589b Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 31 Oct 2024 14:04:38 +0200 Subject: [PATCH 24/30] feat: implement task linked issue soft delete --- .../task-linked-issue.controller.ts | 16 ++++++++ .../linked-issue/task-linked-issue.service.ts | 39 +++++++++++++++---- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts index d475111973c..4d88849a9fd 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts @@ -122,4 +122,20 @@ export class TaskLinkedIssueController extends CrudController { async delete(@Param('id', UUIDValidationPipe) id: ID): Promise { return await this.taskLinkedIssueService.delete(id); } + + @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) + @Delete('/:id/soft') + @UseValidationPipe({ whitelist: true }) + async softRemove(@Param('id', UUIDValidationPipe) id: ID): Promise { + return await this.taskLinkedIssueService.softDelete(id); + } } diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts index 39d442140a0..24bc5d9d3f5 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts @@ -1,6 +1,6 @@ import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { DeleteResult, FindOneOptions } from 'typeorm'; +import { DeleteResult, FindOneOptions, UpdateResult } from 'typeorm'; import { ActionTypeEnum, ActorTypeEnum, @@ -123,6 +123,35 @@ export class TaskLinkedIssueService extends TenantAwareCrudService): Promise { + try { + await this.deleteActivityLog(id); + + return await super.delete(id, options); + } catch (error) { + // Handle errors and return an appropriate error response + throw new HttpException(`Failed to delete task linked issue: ${error.message}`, HttpStatus.BAD_REQUEST); + } + } + + /** + * Soft deletes a task linked issue by its ID, preserving the data while marking it as deleted. + * + * @param {ID} id - The ID of the task linked issue to be soft deleted. + * @returns {Promise} - A promise that resolves to the soft-deleted task linked issue or the update result. + * @throws {HttpException} - Throws an error if the task linked issue cannot be found or the deletion process fails. + */ + async softDelete(id: ID): Promise { + try { + await this.deleteActivityLog(id); + + return await super.softDelete(id); + } catch (error) { + // Handle errors and return an appropriate error response + throw new HttpException(`Failed to delete task linked issue: ${error.message}`, HttpStatus.BAD_REQUEST); + } + } + + private async deleteActivityLog(id: ID) { const tenantId = RequestContext.currentTenantId(); try { // Retrieve existing task linked issue @@ -144,12 +173,6 @@ export class TaskLinkedIssueService extends TenantAwareCrudService Date: Thu, 31 Oct 2024 21:55:30 +0200 Subject: [PATCH 25/30] [Feat] Prevent Double Click on Start/Stop Button (#8516) * feat: add BLOCK_DELAY constant `10 sec` * feat: add pulse animation to start/stop buttons, modified button styles, and refactore timer logic * fix: cspell --- .../src/lib/constants/app.constants.ts | 1 + .../time-tracker/time-tracker.component.html | 37 +++++++++++++++---- .../time-tracker/time-tracker.component.scss | 32 ++++++++++++++++ .../time-tracker/time-tracker.component.ts | 33 ++++++++++++++--- 4 files changed, 89 insertions(+), 14 deletions(-) diff --git a/packages/desktop-ui-lib/src/lib/constants/app.constants.ts b/packages/desktop-ui-lib/src/lib/constants/app.constants.ts index de0c430dfef..467a04102ba 100644 --- a/packages/desktop-ui-lib/src/lib/constants/app.constants.ts +++ b/packages/desktop-ui-lib/src/lib/constants/app.constants.ts @@ -5,6 +5,7 @@ import { environment } from '@gauzy/ui-config'; export const API_PREFIX = '/api'; export const BACKGROUND_SYNC_INTERVAL = 25000; // milliseconds export const BACKGROUND_SYNC_OFFLINE_INTERVAL = 5000; // milliseconds +export const BLOCK_DELAY = 10000; // milliseconds export const GAUZY_ENV = new InjectionToken('gauzyEnvironment'); diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html index 516a3566736..64d2df6ea7f 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html @@ -58,18 +58,39 @@
- -
diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.scss b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.scss index 9849c3ab14d..e78f2719708 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.scss +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.scss @@ -741,3 +741,35 @@ img { } } } + +//animation +@keyframes pulseBorder { + 0% { + box-shadow: 0 0 0 0 var(--pulse-color-rgba); + } + 70% { + box-shadow: 0 0 1rem 1rem rgba(var(--pulse-color-rgb), 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(var(--pulse-color-rgb), 0); + } + } + +.pulse-border { +--pulse-duration: 1.5s; +--pulse-color-rgb: 0, 255, 0; /* Default green color in RGB format */ +--pulse-color-rgba: rgba(var(--pulse-color-rgb), 0.4); +--border-color: rgb(var(--pulse-color-rgb)); + +animation: pulseBorder var(--pulse-duration) infinite; +} + +.start-button { +--pulse-color-rgb: 0, 255, 0; /* Green */ +--pulse-duration: 1s; +} + +.stop-button { +--pulse-color-rgb: 255, 0, 0; /* Red */ +--pulse-duration: 1s; +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts index eda4b0b1b2e..9d919e0c911 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts @@ -40,11 +40,13 @@ import { Observable, of, Subject, - tap + Subscription, + tap, + timer } from 'rxjs'; import { AlwaysOnService, AlwaysOnStateEnum } from '../always-on/always-on.service'; import { AuthStrategy } from '../auth'; -import { GAUZY_ENV } from '../constants'; +import { BLOCK_DELAY, GAUZY_ENV } from '../constants'; import { ElectronService, LoggerService } from '../electron/services'; import { ImageViewerService } from '../image-viewer/image-viewer.service'; import { ActivityWatchViewService } from '../integrations'; @@ -115,6 +117,7 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { private _isReady = false; private _session: moment.Moment = null; private hasActiveTaskPermissions = false; + private timerSubscription: Subscription | null = null; @ViewChild('dialogOpenBtn') btnDialogOpen: ElementRef; public start$: BehaviorSubject = new BehaviorSubject(false); userData: any; @@ -519,8 +522,6 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { } } - this.isProcessingEnabled = false; - asapScheduler.schedule(async () => { try { await this.electronService.ipcRenderer.invoke('UPDATE_SYNCED_TIMER', { @@ -1391,11 +1392,25 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { : 'Please wait for timer to start and make initial screenshot'; this._toastrNotifier.warn(message); this._loggerService.debug(message); + return; } else { this.isProcessingEnabled = true; } this.loading = true; + if (this.timerSubscription) { + this.timerSubscription.unsubscribe(); + } + + this.timerSubscription = timer(BLOCK_DELAY) + .pipe(untilDestroyed(this)) + .subscribe(() => { + this.loading = false; + this.isProcessingEnabled = false; + this._loggerService.info('Processing Unlocked'); + this.timerSubscription = null; + }); + if (!val) { console.log('Stop tracking'); @@ -1515,6 +1530,7 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { this._loggerService.info('Capturing Screen and Sending Activities Start...', activities); await this.takeCaptureAndSendActivities(activities); this._loggerService.info('Capturing Screen and Sending Activities Done ✔️'); + this.isProcessingEnabled = false; }, 1000); } @@ -1536,7 +1552,9 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { this._errorHandlerService.handleError(error); } finally { this.loading = false; - this.isProcessingEnabled = false; + if (this.isRemoteTimer) { + this.isProcessingEnabled = false; + } } } @@ -1590,6 +1608,7 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { await this.uploadScreenshots(activities, timeSlotId, screenshots); this._loggerService.info('Capturing Screen and Sending Activities Done ✔️'); } + this.isProcessingEnabled = false; }, 1000); } } else { @@ -1628,7 +1647,9 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { } finally { this._session = null; this.loading = false; - this.isProcessingEnabled = false; + if (this.isRemoteTimer) { + this.isProcessingEnabled = false; + } this.timeTrackerStore.ignition({ state: IgnitionState.STOPPED, mode: this._startMode }); } } From ab9e03f3c63cd3ad82750fe39b82895e8f1a3ef5 Mon Sep 17 00:00:00 2001 From: adkif <45813955+adkif@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:07:01 +0200 Subject: [PATCH 26/30] chore: target node v20 --- apps/server-api/src/package.json | 6 +++--- apps/server/src/package.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/server-api/src/package.json b/apps/server-api/src/package.json index 6ec43e5b3c4..894b0165ce0 100755 --- a/apps/server-api/src/package.json +++ b/apps/server-api/src/package.json @@ -199,9 +199,9 @@ "node_modules/linebreak/src/classes.trie" ], "targets": [ - "node16-linux-x64", - "node16-mac-x64", - "node16-win-x64" + "node20-linux-x64", + "node20-mac-x64", + "node20-win-x64" ] } } diff --git a/apps/server/src/package.json b/apps/server/src/package.json index d1c932713c2..da5f15e2fc1 100755 --- a/apps/server/src/package.json +++ b/apps/server/src/package.json @@ -212,9 +212,9 @@ "node_modules/linebreak/src/classes.trie" ], "targets": [ - "node16-linux-x64", - "node16-mac-x64", - "node16-win-x64" + "node20-linux-x64", + "node20-mac-x64", + "node20-win-x64" ] } } From 54e99bab09dc86c1522b89b066d01e472543be37 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 5 Nov 2024 05:47:59 +0200 Subject: [PATCH 27/30] fix: rabbit suggestions --- .../src/tasks/linked-issue/task-linked-issue.controller.ts | 4 ++-- .../core/src/tasks/linked-issue/task-linked-issue.service.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts index 4d88849a9fd..9d9a52ce615 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts @@ -118,7 +118,7 @@ export class TaskLinkedIssueController extends CrudController { description: 'Record not found' }) @HttpCode(HttpStatus.ACCEPTED) - @Delete('/:id') + @Delete(':id') async delete(@Param('id', UUIDValidationPipe) id: ID): Promise { return await this.taskLinkedIssueService.delete(id); } @@ -133,7 +133,7 @@ export class TaskLinkedIssueController extends CrudController { description: 'Task Linked Issue record not found' }) @HttpCode(HttpStatus.ACCEPTED) - @Delete('/:id/soft') + @Delete(':id/soft') @UseValidationPipe({ whitelist: true }) async softRemove(@Param('id', UUIDValidationPipe) id: ID): Promise { return await this.taskLinkedIssueService.softDelete(id); diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts index 24bc5d9d3f5..d564f7d3032 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts @@ -173,6 +173,8 @@ export class TaskLinkedIssueService extends TenantAwareCrudService Date: Tue, 5 Nov 2024 11:14:34 +0530 Subject: [PATCH 28/30] fix: improved bit code --- .../task-linked-issue.controller.ts | 48 +++++++++++------ .../linked-issue/task-linked-issue.entity.ts | 8 +-- .../linked-issue/task-linked-issue.helper.ts | 27 +++++----- .../linked-issue/task-linked-issue.service.ts | 53 +++++++++---------- 4 files changed, 75 insertions(+), 61 deletions(-) diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts index 9d9a52ce615..2c194709d4d 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts @@ -15,10 +15,10 @@ 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, PaginationParams } from '../../core/crud'; import { TaskLinkedIssue } from './task-linked-issue.entity'; import { TaskLinkedIssueService } from './task-linked-issue.service'; import { CreateTaskLinkedIssueDTO, UpdateTaskLinkedIssueDTO } from './dto'; @@ -26,16 +26,22 @@ import { TaskLinkedIssueCreateCommand, TaskLinkedIssueUpdateCommand } from './co @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 { constructor( - protected readonly taskLinkedIssueService: TaskLinkedIssueService, + private readonly taskLinkedIssueService: TaskLinkedIssueService, private readonly commandBus: CommandBus ) { super(taskLinkedIssueService); } + /** + * Finds all task linked issues based on the provided query parameters. + * + * @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' }) @@ -51,15 +57,15 @@ export class TaskLinkedIssueController extends CrudController { @Get() @UseValidationPipe() async findAll(@Query() params: PaginationParams): Promise> { - return await this.taskLinkedIssueService.findAll(params); + return this.taskLinkedIssueService.findAll(params); } /** - * Create new Linked Issue + * 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, @@ -67,22 +73,22 @@ export class TaskLinkedIssueController extends CrudController { }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Invalid input, The response body may contain clues as to what went wrong' + 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 { - return await this.commandBus.execute(new TaskLinkedIssueCreateCommand(entity)); + return this.commandBus.execute(new TaskLinkedIssueCreateCommand(entity)); } /** - * Update existing Linked Issue + * Updates an existing task linked issue. * * @param id - The ID of the task linked issue to update. * @param entity - The input data for updating the task linked issue. - * @returns + * @returns A promise that resolves to the updated task linked issue. */ @ApiOperation({ summary: 'Update an existing task linked issue' }) @ApiResponse({ @@ -95,7 +101,7 @@ export class TaskLinkedIssueController extends CrudController { }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Invalid input, The response body may contain clues as to what went wrong' + 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) @@ -105,10 +111,16 @@ export class TaskLinkedIssueController extends CrudController { @Param('id', UUIDValidationPipe) id: ID, @Body() entity: UpdateTaskLinkedIssueDTO ): Promise { - return await this.commandBus.execute(new TaskLinkedIssueUpdateCommand(id, entity)); + return this.commandBus.execute(new TaskLinkedIssueUpdateCommand(id, entity)); } - @ApiOperation({ summary: 'Delete Task Linked issue' }) + /** + * 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' @@ -120,9 +132,15 @@ export class TaskLinkedIssueController extends CrudController { @HttpCode(HttpStatus.ACCEPTED) @Delete(':id') async delete(@Param('id', UUIDValidationPipe) id: ID): Promise { - return await this.taskLinkedIssueService.delete(id); + return this.taskLinkedIssueService.delete(id); } + /** + * 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, @@ -136,6 +154,6 @@ export class TaskLinkedIssueController extends CrudController { @Delete(':id/soft') @UseValidationPipe({ whitelist: true }) async softRemove(@Param('id', UUIDValidationPipe) id: ID): Promise { - return await this.taskLinkedIssueService.softDelete(id); + return this.taskLinkedIssueService.softDelete(id); } } diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.entity.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.entity.ts index c971d2b57cb..9685d0e1ae3 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.entity.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.entity.ts @@ -1,6 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { JoinColumn, RelationId } from 'typeorm'; -import { IsEnum, IsUUID } from 'class-validator'; +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'; @@ -9,9 +9,9 @@ import { MikroOrmTaskLinkedIssueRepository } from './repository/mikro-orm-linked @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; /* @@ -20,6 +20,7 @@ export class TaskLinkedIssue extends TenantOrganizationBaseEntity implements ITa |-------------------------------------------------------------------------- */ @ApiPropertyOptional({ type: () => Task }) + @IsOptional() @MultiORMManyToOne(() => Task) @JoinColumn() taskFrom?: ITask; @@ -35,6 +36,7 @@ export class TaskLinkedIssue extends TenantOrganizationBaseEntity implements ITa * Task Linked Issues */ @ApiPropertyOptional({ type: () => Object }) + @IsOptional() @MultiORMManyToOne(() => Task, (it) => it.linkedIssues) @JoinColumn() taskTo?: ITask; diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.helper.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.helper.ts index a362dcf90be..d869a37ce97 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.helper.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.helper.ts @@ -5,21 +5,20 @@ import { TaskRelatedIssuesRelationEnum } from '@gauzy/contracts'; * * @param {TaskRelatedIssuesRelationEnum} relation - The relation type from the enum `TaskRelatedIssuesRelationEnum`. * @returns {string} The corresponding string description for the given relation type. + * @throws {Error} If the relation type is unsupported. */ export function taskRelatedIssueRelationMap(relation: TaskRelatedIssuesRelationEnum): string { - const issueRelationMap: { [key in TaskRelatedIssuesRelationEnum]: string } = { - [TaskRelatedIssuesRelationEnum.BLOCKS]: 'Blocks', - [TaskRelatedIssuesRelationEnum.CLONES]: 'Clones', - [TaskRelatedIssuesRelationEnum.DUPLICATES]: 'Duplicates', - [TaskRelatedIssuesRelationEnum.IS_BLOCKED_BY]: 'Is Blocked By', - [TaskRelatedIssuesRelationEnum.IS_CLONED_BY]: 'Is Cloned By', - [TaskRelatedIssuesRelationEnum.IS_DUPLICATED_BY]: 'Is Duplicated By', - [TaskRelatedIssuesRelationEnum.RELATES_TO]: 'Relates To' - }; + const issueRelationMap = { + [TaskRelatedIssuesRelationEnum.BLOCKS]: 'Blocks', + [TaskRelatedIssuesRelationEnum.CLONES]: 'Clones', + [TaskRelatedIssuesRelationEnum.DUPLICATES]: 'Duplicates', + [TaskRelatedIssuesRelationEnum.IS_BLOCKED_BY]: 'Is Blocked By', + [TaskRelatedIssuesRelationEnum.IS_CLONED_BY]: 'Is Cloned By', + [TaskRelatedIssuesRelationEnum.IS_DUPLICATED_BY]: 'Is Duplicated By', + [TaskRelatedIssuesRelationEnum.RELATES_TO]: 'Relates To' + } as const; - const issueRelation = issueRelationMap[relation]; - if (!issueRelation) { - throw new Error(`Unsupported relation type: ${relation}`); - } - return issueRelation; + return issueRelationMap[relation] ?? (() => { + throw new Error(`Unsupported relation type: ${relation}`); + })(); } diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts index d564f7d3032..6ca88ce08e6 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.service.ts @@ -1,5 +1,4 @@ import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; import { DeleteResult, FindOneOptions, UpdateResult } from 'typeorm'; import { ActionTypeEnum, @@ -21,11 +20,8 @@ import { taskRelatedIssueRelationMap } from './task-linked-issue.helper'; @Injectable() export class TaskLinkedIssueService extends TenantAwareCrudService { constructor( - @InjectRepository(TaskLinkedIssue) typeOrmTaskLinkedIssueRepository: TypeOrmTaskLinkedIssueRepository, - mikroOrmTaskLinkedIssueRepository: MikroOrmTaskLinkedIssueRepository, - private readonly activityLogService: ActivityLogService ) { super(typeOrmTaskLinkedIssueRepository, mikroOrmTaskLinkedIssueRepository); @@ -113,53 +109,52 @@ export class TaskLinkedIssueService extends TenantAwareCrudService} [options] - Optional query options to find the task linked issue before deletion. - * @returns {Promise} The result of the deletion operation. - * @throws {HttpException} Throws a Bad Request exception if the deletion fails. - * @throws {NotFoundException} Throws a Not Found exception if the task linked issue does not exist. + * Deletes a task linked issue and logs the deletion activity. * + * @param id - The ID of the task linked issue to delete. + * @param options - Optional find options for the task linked issue. + * @returns A promise that resolves to the result of the delete operation. */ async delete(id: ID, options?: FindOneOptions): Promise { try { await this.deleteActivityLog(id); - - return await super.delete(id, options); + return super.delete(id, options); } catch (error) { - // Handle errors and return an appropriate error response + console.error(`Failed to delete task linked issue (ID: ${id}):`, error); throw new HttpException(`Failed to delete task linked issue: ${error.message}`, HttpStatus.BAD_REQUEST); } } /** - * Soft deletes a task linked issue by its ID, preserving the data while marking it as deleted. + * Soft deletes a task linked issue and logs the deletion activity. * - * @param {ID} id - The ID of the task linked issue to be soft deleted. - * @returns {Promise} - A promise that resolves to the soft-deleted task linked issue or the update result. - * @throws {HttpException} - Throws an error if the task linked issue cannot be found or the deletion process fails. + * @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 or the deleted entity. */ async softDelete(id: ID): Promise { try { await this.deleteActivityLog(id); - - return await super.softDelete(id); + return super.softDelete(id); } catch (error) { - // Handle errors and return an appropriate error response - throw new HttpException(`Failed to delete task linked issue: ${error.message}`, HttpStatus.BAD_REQUEST); + console.error(`Failed to soft delete task linked issue (ID: ${id}):`, error); + throw new HttpException(`Failed to soft delete task linked issue: ${error.message}`, HttpStatus.BAD_REQUEST); } } + /** + * Deletes an activity log for a given task linked issue. + * + * @param id - The ID of the task linked issue to delete. + */ private async deleteActivityLog(id: ID) { const tenantId = RequestContext.currentTenantId(); - try { - // Retrieve existing task linked issue - const existingTaskLinkedIssue = await this.findOneByIdString(id); - if (!existingTaskLinkedIssue) { - throw new NotFoundException('Task linked issue not found'); - } + try { + // Retrieve existing task linked issue + const existingTaskLinkedIssue = await this.findOneByIdString(id); + if (!existingTaskLinkedIssue) { + throw new NotFoundException('Task linked issue not found'); + } // Generate deleted activity log const { organizationId } = existingTaskLinkedIssue; @@ -174,7 +169,7 @@ export class TaskLinkedIssueService extends TenantAwareCrudService Date: Tue, 5 Nov 2024 11:48:28 +0530 Subject: [PATCH 29/30] fix: Nest can't resolve dependencies --- .../core/src/tasks/linked-issue/task-linked-issue.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.module.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.module.ts index 583c0ed116b..fda06ec6a8c 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.module.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.module.ts @@ -8,6 +8,7 @@ import { TaskLinkedIssue } from './task-linked-issue.entity'; import { CommandHandlers } from './commands/handlers'; import { TaskLinkedIssueController } from './task-linked-issue.controller'; import { TaskLinkedIssueService } from './task-linked-issue.service'; +import { TypeOrmTaskLinkedIssueRepository } from './repository/type-orm-linked-issue.repository'; @Module({ imports: [ @@ -18,7 +19,7 @@ import { TaskLinkedIssueService } from './task-linked-issue.service'; CqrsModule ], controllers: [TaskLinkedIssueController], - providers: [TaskLinkedIssueService, ...CommandHandlers], + providers: [TaskLinkedIssueService, TypeOrmTaskLinkedIssueRepository, ...CommandHandlers], exports: [TaskLinkedIssueService] }) export class TaskLinkedIssueModule {} From 25e8828c13553a5a47e4b103b9ae00bfa1c18817 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 5 Nov 2024 08:38:52 +0200 Subject: [PATCH 30/30] fix: task linked issue missing permissions --- .../src/tasks/linked-issue/task-linked-issue.controller.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts index 2c194709d4d..ad05dae1438 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts @@ -54,6 +54,7 @@ export class TaskLinkedIssueController extends CrudController { status: HttpStatus.NOT_FOUND, description: 'Record not found' }) + @Permissions(PermissionsEnum.ALL_ORG_VIEW, PermissionsEnum.ORG_TASK_VIEW) @Get() @UseValidationPipe() async findAll(@Query() params: PaginationParams): Promise> { @@ -130,6 +131,7 @@ export class TaskLinkedIssueController extends CrudController { 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 { return this.taskLinkedIssueService.delete(id); @@ -151,6 +153,7 @@ export class TaskLinkedIssueController extends CrudController { 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 {