diff --git a/libs/event-emitter/src/event-emitter.module.ts b/libs/event-emitter/src/event-emitter.module.ts index 60a308a..f5b16f2 100644 --- a/libs/event-emitter/src/event-emitter.module.ts +++ b/libs/event-emitter/src/event-emitter.module.ts @@ -26,6 +26,7 @@ import { IssueUrlEnhancer } from './issue-enhancers/issue-url-enhancer'; import { ListIssuesByUsersWidgetService } from './project-dashboard/widgets/list-issues-by-users.widget.service'; import { ListIssuesByUsersLikeJiraWidgetService } from './project-dashboard/widgets/list-issues-by-users-like-jira.widget.service'; import { TimePassedHighlightEnhancer } from './issue-enhancers/time-passed-highlight-enhancer'; +import { ListIssuesByFieldsWidgetService } from './project-dashboard/widgets/list-issues-by-fields.widget.service'; @Module({}) export class EventEmitterModule implements OnModuleInit { @@ -56,6 +57,7 @@ export class EventEmitterModule implements OnModuleInit { ListIssuesByUsersWidgetService, ListIssuesByUsersLikeJiraWidgetService, TimePassedHighlightEnhancer, + ListIssuesByFieldsWidgetService, ], exports: [ EventEmitterService, @@ -78,6 +80,7 @@ export class EventEmitterModule implements OnModuleInit { ListIssuesByUsersWidgetService, ListIssuesByUsersLikeJiraWidgetService, TimePassedHighlightEnhancer, + ListIssuesByFieldsWidgetService, ], controllers: [MainController, UsersController, IssuesController], }; diff --git a/libs/event-emitter/src/models/redmine-types.ts b/libs/event-emitter/src/models/redmine-types.ts index bbde0c7..00471c3 100644 --- a/libs/event-emitter/src/models/redmine-types.ts +++ b/libs/event-emitter/src/models/redmine-types.ts @@ -44,7 +44,7 @@ export module RedmineTypes { author: IdAndName; assigned_to?: IdAndName; category: IdAndName; - fixed_version: IdAndName; + fixed_version?: IdAndName; subject: string; description: string; start_date: string; diff --git a/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-fields.widget.service.ts b/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-fields.widget.service.ts new file mode 100644 index 0000000..59d3792 --- /dev/null +++ b/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-fields.widget.service.ts @@ -0,0 +1,133 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +import { IssueUrlEnhancer } from '@app/event-emitter/issue-enhancers/issue-url-enhancer'; +import { TagStyledEnhancerNs } from '@app/event-emitter/issue-enhancers/tag-styled-enhancer'; +import { TimePassedHighlightEnhancer } from '@app/event-emitter/issue-enhancers/time-passed-highlight-enhancer'; +import { + IssuesService, + IssuesServiceNs, +} from '@app/event-emitter/issues/issues.service'; +import { RedmineTypes } from '@app/event-emitter/models/redmine-types'; +import { FlatIssuesStore } from '@app/event-emitter/utils/flat-issues-store'; +import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from-object-by-key'; +import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store'; +import { Injectable, Logger } from '@nestjs/common'; +import nano from 'nano'; +import { WidgetInterface } from '../widget-interface'; + +export namespace ListIssuesByFieldsWidgetNs { + export type Params = { + fromRootIssueId?: number; + fromQuery?: nano.MangoQuery; + fields: Field[]; + delimiter: string; + sort?: boolean; + statuses: string[]; + } & TagStyledEnhancerNs.ConfigWithTagsStyles; + + export type Field = { + path: string; + default: string; + }; +} + +type Params = ListIssuesByFieldsWidgetNs.Params; + +@Injectable() +export class ListIssuesByFieldsWidgetService + implements WidgetInterface +{ + private logger = new Logger(ListIssuesByFieldsWidgetService.name); + private issuesLoader: IssuesServiceNs.IssuesLoader; + + constructor( + private issuesService: IssuesService, + private timePassedHighlightEnhancer: TimePassedHighlightEnhancer, + private issueUrlEnhancer: IssueUrlEnhancer, + ) { + this.issuesLoader = this.issuesService.createDynamicIssuesLoader(); + } + + isMyConfig(): boolean { + return true; + } + + async render(widgetParams: Params): Promise { + let store: FlatIssuesStore; + if (widgetParams.fromRootIssueId) { + store = await this.getListFromRoot(widgetParams.fromRootIssueId); + } else if (widgetParams.fromQuery) { + store = await this.getListByQuery(widgetParams.fromQuery); + } else { + const errMsg = `Wrong widgetParams value`; + this.logger.error(errMsg); + throw new Error(errMsg); + } + await store.enhanceIssues([ + this.timePassedHighlightEnhancer, + this.issueUrlEnhancer, + TagStyledEnhancerNs.CreateTagStyledEnhancerForConfig(widgetParams), + ]); + const grouped = store.groupByStatusWithExtra((issue) => { + return this.getGroupFromIssue(issue, widgetParams); + }, widgetParams.statuses); + let res = [] as any[]; + for (const key in grouped) { + if (Object.prototype.hasOwnProperty.call(grouped, key)) { + const data = grouped[key]; + res.push({ + data: data, + metainfo: this.createMetaInfo(key), + }); + } + } + if (widgetParams.sort) { + res = res.sort((a, b) => { + return a.metainfo.title.localeCompare(b.metainfo.title); + }); + } + return res; + } + + private async getListFromRoot(issueId: number): Promise { + const treeStore = new TreeIssuesStore(); + const rootIssue = await this.issuesService.getIssue(issueId); + treeStore.setRootIssue(rootIssue); + await treeStore.fillData(this.issuesLoader); + return treeStore.getFlatStore(); + } + + private async getListByQuery( + query: nano.MangoQuery, + ): Promise { + const rawData = await this.issuesService.find(query); + const store = new FlatIssuesStore(); + for (let i = 0; i < rawData.length; i++) { + const issue = rawData[i]; + store.push(issue); + } + return store; + } + + private getGroupFromIssue( + issue: RedmineTypes.ExtendedIssue, + params: Params, + ): string | null { + if (!issue) return null; + const values = [] as string[]; + for (let i = 0; i < params.fields.length; i++) { + const field = params.fields[i]; + const valueResult = GetValueFromObjectByKey(issue, field.path); + const value: string = valueResult.result + ? valueResult.result + : field.default; + values.push(value); + } + return values.join(params.delimiter); + } + + private createMetaInfo(title: string): Record { + return { + title: title, + }; + } +} diff --git a/src/dashboards/simple-kanban-board.controller.ts b/src/dashboards/simple-kanban-board.controller.ts index aafc98f..867b11f 100644 --- a/src/dashboards/simple-kanban-board.controller.ts +++ b/src/dashboards/simple-kanban-board.controller.ts @@ -1,5 +1,6 @@ import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader'; import { RedmineEventsGateway } from '@app/event-emitter/events/redmine-events.gateway'; +import { ListIssuesByFieldsWidgetService } from '@app/event-emitter/project-dashboard/widgets/list-issues-by-fields.widget.service'; import { ListIssuesByUsersLikeJiraWidgetService } from '@app/event-emitter/project-dashboard/widgets/list-issues-by-users-like-jira.widget.service'; import { ListIssuesByUsersWidgetService } from '@app/event-emitter/project-dashboard/widgets/list-issues-by-users.widget.service'; import { @@ -23,6 +24,7 @@ export class SimpleKanbanBoardController { private listIssuesByUsersLikeJiraWidgetService: ListIssuesByUsersLikeJiraWidgetService, private issuesByTagsWidgetService: IssuesByTagsWidgetService, private redmineEventsGateway: RedmineEventsGateway, + private listIssuesByFieldsWidgetService: ListIssuesByFieldsWidgetService, ) { this.path = this.configService.get('simpleKanbanBoard.path'); } @@ -109,4 +111,20 @@ export class SimpleKanbanBoardController { async getByTags(@Param('name') name: string): Promise { return await this.getByTagsRawData(name); } + + @Get('/by-fields/:name/raw') + async getByFieldsRawData(@Param('name') name: string): Promise { + const cfg = this.dynamicLoader.load(name, { + path: this.path, + ext: 'jsonc', + parser: parse, + }); + return await this.listIssuesByFieldsWidgetService.render(cfg); + } + + @Get('/by-fields/:name') + @Render('simple-kanban-board') + async getByFields(@Param('name') name: string): Promise { + return await this.getByFieldsRawData(name); + } }