From 1f3964f5cafe55c974097038af3f428aef66bf28 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Mon, 13 Feb 2023 08:10:51 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D0=B4=D0=BE?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=20=D0=BF=D0=BE=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BD=D0=B8=D0=BA=D0=B0=D0=BC=20=D0=BA=D0=B0=D0=BA=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=80=D0=B0=D1=8F,=20=D1=82=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BA=D0=BE=20=D0=BA=D0=B0=D0=BA=20=D0=B2=20jira?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event-emitter/src/event-emitter.module.ts | 3 + .../src/issues/issues.service.ts | 22 ++++ ...ssues-by-users-like-jira.widget.service.ts | 109 ++++++++++++++++++ .../list-issues-by-users.widget.service.ts | 39 +++---- .../root-issue-subtrees.widget.service.ts | 26 ++--- .../src/utils/flat-issues-store.ts | 42 ++++++- .../src/utils/get-value-from-object-by-key.ts | 15 +++ .../simple-kanban-board.controller.ts | 18 +++ 8 files changed, 230 insertions(+), 44 deletions(-) create mode 100644 libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users-like-jira.widget.service.ts create mode 100644 libs/event-emitter/src/utils/get-value-from-object-by-key.ts diff --git a/libs/event-emitter/src/event-emitter.module.ts b/libs/event-emitter/src/event-emitter.module.ts index c2113c3..a177e88 100644 --- a/libs/event-emitter/src/event-emitter.module.ts +++ b/libs/event-emitter/src/event-emitter.module.ts @@ -24,6 +24,7 @@ import { DynamicLoader } from './configs/dynamic-loader'; import { RedminePublicUrlConverter } from './converters/redmine-public-url.converter'; 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'; @Module({}) export class EventEmitterModule implements OnModuleInit { @@ -52,6 +53,7 @@ export class EventEmitterModule implements OnModuleInit { RedminePublicUrlConverter, IssueUrlEnhancer, ListIssuesByUsersWidgetService, + ListIssuesByUsersLikeJiraWidgetService, ], exports: [ EventEmitterService, @@ -72,6 +74,7 @@ export class EventEmitterModule implements OnModuleInit { RedminePublicUrlConverter, IssueUrlEnhancer, ListIssuesByUsersWidgetService, + ListIssuesByUsersLikeJiraWidgetService, ], controllers: [MainController, UsersController, IssuesController], }; diff --git a/libs/event-emitter/src/issues/issues.service.ts b/libs/event-emitter/src/issues/issues.service.ts index 6b04cd7..3795622 100644 --- a/libs/event-emitter/src/issues/issues.service.ts +++ b/libs/event-emitter/src/issues/issues.service.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-namespace */ import { RedmineTypes } from '../models/redmine-types'; import { CacheTTL, Injectable, Logger } from '@nestjs/common'; import { Issues } from '../couchdb-datasources/issues'; @@ -12,6 +13,12 @@ import { GetParentsHint } from '../utils/get-parents-hint'; export const ISSUE_MEMORY_CACHE_LIFETIME = 30 * 1000; const ISSUE_MEMORY_CACHE_AUTOCLEAN_INTERVAL = 1000 * 60 * 5; +export namespace IssuesServiceNs { + export type IssuesLoader = ( + ids: number[], + ) => Promise>; +} + @Injectable() export class IssuesService { private logger = new Logger(IssuesService.name); @@ -143,4 +150,19 @@ export class IssuesService { return null; } } + + createDynamicIssuesLoader(): IssuesServiceNs.IssuesLoader { + const fn = async ( + ids: number[], + ): Promise> => { + const issues = await this.getIssues(ids); + const res = {} as Record; + for (let i = 0; i < issues.length; i++) { + const issue = issues[i]; + res[issue.id] = issue; + } + return res; + }; + return fn; + } } diff --git a/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users-like-jira.widget.service.ts b/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users-like-jira.widget.service.ts new file mode 100644 index 0000000..42e0829 --- /dev/null +++ b/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users-like-jira.widget.service.ts @@ -0,0 +1,109 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +import { + IssuesService, + IssuesServiceNs, +} from '@app/event-emitter/issues/issues.service'; +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 ListIssuesByUsersLikeJiraWidgetNs { + export namespace Models { + export type Params = { + fromRootIssueId?: number; + fromQuery?: nano.MangoQuery; + userKeys: string[]; + userSort?: boolean; + statuses: string[]; + }; + } +} + +type Params = ListIssuesByUsersLikeJiraWidgetNs.Models.Params; + +@Injectable() +export class ListIssuesByUsersLikeJiraWidgetService + implements WidgetInterface +{ + private logger = new Logger(ListIssuesByUsersLikeJiraWidgetService.name); + private issuesLoader: IssuesServiceNs.IssuesLoader; + + constructor(private issuesService: IssuesService) { + 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); + } + const grouped = store.groupByStatusWithExtraToMultipleStories((issue) => { + const users = [] as string[]; + for (let i = 0; i < widgetParams.userKeys.length; i++) { + const userKey = widgetParams.userKeys[i]; + const userValue = GetValueFromObjectByKey(issue, userKey); + if (userValue.result) { + users.push(userValue.result); + } else { + users.push('Unknown Unknown'); + } + } + return users; + }, widgetParams.statuses); + let res = [] as any[]; + for (const user in grouped) { + if (Object.prototype.hasOwnProperty.call(grouped, user)) { + const data = grouped[user]; + res.push({ + data: data, + metainfo: this.createMetaInfo(user), + }); + } + } + if (widgetParams.userSort) { + 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 createMetaInfo(user: string): Record { + return { + title: user, + }; + } +} diff --git a/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users.widget.service.ts b/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users.widget.service.ts index 9f643ed..0ab5c4c 100644 --- a/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users.widget.service.ts +++ b/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users.widget.service.ts @@ -1,7 +1,11 @@ /* eslint-disable @typescript-eslint/no-namespace */ -import { IssuesService } from '@app/event-emitter/issues/issues.service'; +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'; @@ -37,9 +41,10 @@ export class ListIssuesByUsersWidgetService implements WidgetInterface { private logger = new Logger(ListIssuesByUsersWidgetService.name); + private issuesLoader: IssuesServiceNs.IssuesLoader; constructor(private issuesService: IssuesService) { - return; + this.issuesLoader = this.issuesService.createDynamicIssuesLoader(); } isMyConfig(): boolean { @@ -54,7 +59,7 @@ export class ListIssuesByUsersWidgetService store = await this.getListByQuery(widgetParams.fromQuery); } else { const errMsg = `Wrong widgetParams value`; - this.logger.error(`Wrong widgetParams value`); + this.logger.error(errMsg); throw new Error(errMsg); } const grouped = store.groupByStatusWithExtra((issue) => { @@ -83,7 +88,7 @@ export class ListIssuesByUsersWidgetService const treeStore = new TreeIssuesStore(); const rootIssue = await this.issuesService.getIssue(issueId); treeStore.setRootIssue(rootIssue); - await treeStore.fillData(this.issuesLoader.bind(this)); + await treeStore.fillData(this.issuesLoader); return treeStore.getFlatStore(); } @@ -99,29 +104,13 @@ export class ListIssuesByUsersWidgetService return store; } - private async issuesLoader( - ids: number[], - ): Promise> { - const issues = await this.issuesService.getIssues(ids); - const res = {} as Record; - for (let i = 0; i < issues.length; i++) { - const issue = issues[i]; - res[issue.id] = issue; - } - return res; - } - private getUserValueByKey(issue: ExtendedIssue, key: string): FindResult { - const keys = key.split('.'); - let res: any = issue; - for (let i = 0; i < keys.length; i++) { - const k = keys[i]; - if (!res.hasOwnProperty(k)) { - return { error: ListIssuesByUsersWidgetNs.Models.FindErrors.NOT_FOUND }; - } - res = res[k]; + const value = GetValueFromObjectByKey(issue, key); + if (value.result) { + return { result: value.result }; + } else { + return { error: ListIssuesByUsersWidgetNs.Models.FindErrors.NOT_FOUND }; } - return { result: res }; } private createMetaInfo(user: string): Record { diff --git a/libs/event-emitter/src/project-dashboard/widgets/root-issue-subtrees.widget.service.ts b/libs/event-emitter/src/project-dashboard/widgets/root-issue-subtrees.widget.service.ts index f7fe98c..a120c60 100644 --- a/libs/event-emitter/src/project-dashboard/widgets/root-issue-subtrees.widget.service.ts +++ b/libs/event-emitter/src/project-dashboard/widgets/root-issue-subtrees.widget.service.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-namespace */ -import { IssuesService } from '@app/event-emitter/issues/issues.service'; -import { RedmineTypes } from '@app/event-emitter/models/redmine-types'; +import { + IssuesService, + IssuesServiceNs, +} from '@app/event-emitter/issues/issues.service'; import { TreeIssuesStore, TreeIssuesStoreNs, @@ -36,7 +38,11 @@ type Params = RootIssueSubTreesWidgetNs.Models.Params; export class RootIssueSubTreesWidgetService implements WidgetInterface { - constructor(private issuesService: IssuesService) {} + private issuesLoader: IssuesServiceNs.IssuesLoader; + + constructor(private issuesService: IssuesService) { + this.issuesLoader = this.issuesService.createDynamicIssuesLoader(); + } isMyConfig(): boolean { return true; @@ -48,7 +54,7 @@ export class RootIssueSubTreesWidgetService widgetParams.rootIssueId, ); treeStore.setRootIssue(rootIssue); - await treeStore.fillData(this.issuesLoader.bind(this)); + await treeStore.fillData(this.issuesLoader); let stories: TreeIssuesStoreNs.Models.GetFlatStories.Result; if (widgetParams.parentsAsGroups) { stories = treeStore.getFlatStoriesByParents(); @@ -76,16 +82,4 @@ export class RootIssueSubTreesWidgetService }; }); } - - private async issuesLoader( - ids: number[], - ): Promise> { - const issues = await this.issuesService.getIssues(ids); - const res = {} as Record; - for (let i = 0; i < issues.length; i++) { - const issue = issues[i]; - res[issue.id] = issue; - } - return res; - } } diff --git a/libs/event-emitter/src/utils/flat-issues-store.ts b/libs/event-emitter/src/utils/flat-issues-store.ts index a9b07a3..ab29f66 100644 --- a/libs/event-emitter/src/utils/flat-issues-store.ts +++ b/libs/event-emitter/src/utils/flat-issues-store.ts @@ -1,10 +1,9 @@ /* eslint-disable @typescript-eslint/no-namespace */ +import { IssuesServiceNs } from '../issues/issues.service'; import { RedmineTypes } from '../models/redmine-types'; export namespace FlatIssuesStoreNs { - export type IssuesLoader = ( - ids: number[], - ) => Promise>; + export type IssuesLoader = IssuesServiceNs.IssuesLoader; export namespace Models { export type ByStatus = { @@ -178,4 +177,41 @@ export class FlatIssuesStore { } return res; } + + groupByToMultipleStories( + iteratee: (issue: RedmineTypes.Issue) => (string | number)[], + ): Record { + const res = {} as Record; + const items = this.getIssues(); + for (let i = 0; i < items.length; i++) { + const issue = items[i]; + const keys = iteratee(issue); + for (let j = 0; j < keys.length; j++) { + const key = keys[j]; + if (!Object.prototype.hasOwnProperty.call(res, key)) { + res[key] = new FlatIssuesStore(); + } + res[key].push(issue); + } + } + return res; + } + + groupByStatusWithExtraToMultipleStories( + iteratee: (issue: RedmineTypes.Issue) => (string | number)[], + statuses: string[], + ): Record { + const res = {} as Record< + string | number, + FlatIssuesStoreNs.Models.ByStatuses + >; + const groupedIssues = this.groupByToMultipleStories(iteratee); + for (const key in groupedIssues) { + if (Object.prototype.hasOwnProperty.call(groupedIssues, key)) { + const store = groupedIssues[key]; + res[key] = store.groupByStatus(statuses); + } + } + return res; + } } diff --git a/libs/event-emitter/src/utils/get-value-from-object-by-key.ts b/libs/event-emitter/src/utils/get-value-from-object-by-key.ts new file mode 100644 index 0000000..412b845 --- /dev/null +++ b/libs/event-emitter/src/utils/get-value-from-object-by-key.ts @@ -0,0 +1,15 @@ +export function GetValueFromObjectByKey( + obj: any, + key: string, +): { result?: any; error?: string } { + const keys = key.split('.'); + let res: any = obj; + for (let i = 0; i < keys.length; i++) { + const k = keys[i]; + if (!res.hasOwnProperty(k)) { + return { error: 'NOT_FOUND' }; + } + res = res[k]; + } + return { result: res }; +} diff --git a/src/dashboards/simple-kanban-board.controller.ts b/src/dashboards/simple-kanban-board.controller.ts index f8fb993..09d7fc8 100644 --- a/src/dashboards/simple-kanban-board.controller.ts +++ b/src/dashboards/simple-kanban-board.controller.ts @@ -1,4 +1,5 @@ import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader'; +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 { RootIssueSubTreesWidgetService } from '@app/event-emitter/project-dashboard/widgets/root-issue-subtrees.widget.service'; import { Controller, Get, Param, Render } from '@nestjs/common'; @@ -14,6 +15,7 @@ export class SimpleKanbanBoardController { private dynamicLoader: DynamicLoader, private configService: ConfigService, private listIssuesByUsersWidgetService: ListIssuesByUsersWidgetService, + private listIssuesByUsersLikeJiraWidgetService: ListIssuesByUsersLikeJiraWidgetService, ) { this.path = this.configService.get('simpleKanbanBoard.path'); } @@ -49,4 +51,20 @@ export class SimpleKanbanBoardController { async getByUsers(@Param('name') name: string): Promise { return await this.getByUsersRawData(name); } + + @Get('/by-users-like-jira/:name/raw') + async getByUsersLikeJiraRawData(@Param('name') name: string): Promise { + const cfg = this.dynamicLoader.load(name, { + path: this.path, + ext: 'jsonc', + parser: parse, + }); + return await this.listIssuesByUsersLikeJiraWidgetService.render(cfg); + } + + @Get('/by-users-like-jira/:name') + @Render('simple-kanban-board') + async getByUsersLikeJira(@Param('name') name: string): Promise { + return await this.getByUsersLikeJiraRawData(name); + } }