From 2aac9ae94c689e452647fe09ae4c2f25b4c573bb Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Mon, 13 Feb 2023 21:09:36 +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=20=D1=86=D0=B2=D0=B5=D1=82=D0=BE=D0=B2=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=B8=D0=BD=D0=B4=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D1=80=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8=20=D1=81=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=D0=B4=D0=BD=D0=B5=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event-emitter/src/event-emitter.module.ts | 3 + .../time-passed-highlight-enhancer.ts | 68 +++++++++++++++++++ .../event-emitter/src/models/redmine-types.ts | 2 + ...ssues-by-users-like-jira.widget.service.ts | 8 ++- .../list-issues-by-users.widget.service.ts | 8 ++- .../root-issue-subtrees.widget.service.ts | 7 +- .../src/utils/flat-issues-store.ts | 14 ++++ .../src/utils/tree-issues-store.ts | 5 ++ views/simple-kanban-board.hbs | 24 ++++++- 9 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 libs/event-emitter/src/issue-enhancers/time-passed-highlight-enhancer.ts diff --git a/libs/event-emitter/src/event-emitter.module.ts b/libs/event-emitter/src/event-emitter.module.ts index a177e88..60a308a 100644 --- a/libs/event-emitter/src/event-emitter.module.ts +++ b/libs/event-emitter/src/event-emitter.module.ts @@ -25,6 +25,7 @@ import { RedminePublicUrlConverter } from './converters/redmine-public-url.conve 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'; @Module({}) export class EventEmitterModule implements OnModuleInit { @@ -54,6 +55,7 @@ export class EventEmitterModule implements OnModuleInit { IssueUrlEnhancer, ListIssuesByUsersWidgetService, ListIssuesByUsersLikeJiraWidgetService, + TimePassedHighlightEnhancer, ], exports: [ EventEmitterService, @@ -75,6 +77,7 @@ export class EventEmitterModule implements OnModuleInit { IssueUrlEnhancer, ListIssuesByUsersWidgetService, ListIssuesByUsersLikeJiraWidgetService, + TimePassedHighlightEnhancer, ], controllers: [MainController, UsersController, IssuesController], }; diff --git a/libs/event-emitter/src/issue-enhancers/time-passed-highlight-enhancer.ts b/libs/event-emitter/src/issue-enhancers/time-passed-highlight-enhancer.ts new file mode 100644 index 0000000..4bd6a17 --- /dev/null +++ b/libs/event-emitter/src/issue-enhancers/time-passed-highlight-enhancer.ts @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +import { Injectable } from '@nestjs/common'; +import { RedmineTypes } from '../models/redmine-types'; +import { TimestampConverter } from '../utils/timestamp-converter'; +import { IssueEnhancerInterface } from './issue-enhancer-interface'; + +export namespace TimePassedHighlightEnhancerNs { + export type PriorityRules = { + /** time in seconds */ + timePassed: number; + priority: string; + }; +} + +@Injectable() +export class TimePassedHighlightEnhancer implements IssueEnhancerInterface { + name = 'activity-to-priority'; + + private rules: TimePassedHighlightEnhancerNs.PriorityRules[] = [ + { + timePassed: 60 * 60, // 1 час + priority: 'hot', + }, + { + timePassed: 24 * 60 * 60, // 1 день, + priority: 'warm', + }, + { + timePassed: 7 * 24 * 60 * 60, // 1 неделя + priority: 'comfort', + }, + { + timePassed: 14 * 24 * 60 * 60, // 2 недели + priority: 'breezy', + }, + ]; + + private otherPriority = 'cold'; + + private keyNameForCssClass = 'timePassedClass'; + + constructor() { + this.rules = this.rules.sort((a, b) => { + return a.timePassed - b.timePassed; + }); + } + + async enhance( + issue: RedmineTypes.ExtendedIssue, + ): Promise { + const nowTimestamp = new Date().getTime(); + if (!issue?.updated_on) return issue; + for (let i = 0; i < this.rules.length; i++) { + const rule = this.rules[i]; + if ( + nowTimestamp - TimestampConverter.toTimestamp(issue.updated_on) <= + rule.timePassed * 1000 + ) { + issue[this.keyNameForCssClass] = rule.priority; + break; + } + } + if (!issue[this.keyNameForCssClass]) { + issue[this.keyNameForCssClass] = this.otherPriority; + } + return issue; + } +} diff --git a/libs/event-emitter/src/models/redmine-types.ts b/libs/event-emitter/src/models/redmine-types.ts index 12f1d9d..bbde0c7 100644 --- a/libs/event-emitter/src/models/redmine-types.ts +++ b/libs/event-emitter/src/models/redmine-types.ts @@ -61,6 +61,8 @@ export module RedmineTypes { parent?: { id: number }; }; + export type ExtendedIssue = Issue & Record; + // eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace export module Unknown { export const num = -1; 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 index 42e0829..f40c90c 100644 --- 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 @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-namespace */ +import { TimePassedHighlightEnhancer } from '@app/event-emitter/issue-enhancers/time-passed-highlight-enhancer'; import { IssuesService, IssuesServiceNs, @@ -31,7 +32,10 @@ export class ListIssuesByUsersLikeJiraWidgetService private logger = new Logger(ListIssuesByUsersLikeJiraWidgetService.name); private issuesLoader: IssuesServiceNs.IssuesLoader; - constructor(private issuesService: IssuesService) { + constructor( + private issuesService: IssuesService, + private timePassedHighlightEnhancer: TimePassedHighlightEnhancer, + ) { this.issuesLoader = this.issuesService.createDynamicIssuesLoader(); } @@ -86,6 +90,7 @@ export class ListIssuesByUsersLikeJiraWidgetService const rootIssue = await this.issuesService.getIssue(issueId); treeStore.setRootIssue(rootIssue); await treeStore.fillData(this.issuesLoader); + await treeStore.enhanceIssues([this.timePassedHighlightEnhancer]); return treeStore.getFlatStore(); } @@ -98,6 +103,7 @@ export class ListIssuesByUsersLikeJiraWidgetService const issue = rawData[i]; store.push(issue); } + await store.enhanceIssues([this.timePassedHighlightEnhancer]); return store; } 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 0ab5c4c..34374fd 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,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-namespace */ +import { TimePassedHighlightEnhancer } from '@app/event-emitter/issue-enhancers/time-passed-highlight-enhancer'; import { IssuesService, IssuesServiceNs, @@ -43,7 +44,10 @@ export class ListIssuesByUsersWidgetService private logger = new Logger(ListIssuesByUsersWidgetService.name); private issuesLoader: IssuesServiceNs.IssuesLoader; - constructor(private issuesService: IssuesService) { + constructor( + private issuesService: IssuesService, + private timePassedHighlightEnhancer: TimePassedHighlightEnhancer, + ) { this.issuesLoader = this.issuesService.createDynamicIssuesLoader(); } @@ -89,6 +93,7 @@ export class ListIssuesByUsersWidgetService const rootIssue = await this.issuesService.getIssue(issueId); treeStore.setRootIssue(rootIssue); await treeStore.fillData(this.issuesLoader); + await treeStore.enhanceIssues([this.timePassedHighlightEnhancer]); return treeStore.getFlatStore(); } @@ -101,6 +106,7 @@ export class ListIssuesByUsersWidgetService const issue = rawData[i]; store.push(issue); } + await store.enhanceIssues([this.timePassedHighlightEnhancer]); return store; } 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 a120c60..62ea77e 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,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-namespace */ +import { TimePassedHighlightEnhancer } from '@app/event-emitter/issue-enhancers/time-passed-highlight-enhancer'; import { IssuesService, IssuesServiceNs, @@ -40,7 +41,10 @@ export class RootIssueSubTreesWidgetService { private issuesLoader: IssuesServiceNs.IssuesLoader; - constructor(private issuesService: IssuesService) { + constructor( + private issuesService: IssuesService, + private timePassedHighlightEnhancer: TimePassedHighlightEnhancer, + ) { this.issuesLoader = this.issuesService.createDynamicIssuesLoader(); } @@ -55,6 +59,7 @@ export class RootIssueSubTreesWidgetService ); treeStore.setRootIssue(rootIssue); await treeStore.fillData(this.issuesLoader); + await treeStore.enhanceIssues([this.timePassedHighlightEnhancer]); let stories: TreeIssuesStoreNs.Models.GetFlatStories.Result; if (widgetParams.parentsAsGroups) { stories = treeStore.getFlatStoriesByParents(); diff --git a/libs/event-emitter/src/utils/flat-issues-store.ts b/libs/event-emitter/src/utils/flat-issues-store.ts index ab29f66..21483ca 100644 --- a/libs/event-emitter/src/utils/flat-issues-store.ts +++ b/libs/event-emitter/src/utils/flat-issues-store.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-namespace */ +import { IssueEnhancerInterface } from '../issue-enhancers/issue-enhancer-interface'; import { IssuesServiceNs } from '../issues/issues.service'; import { RedmineTypes } from '../models/redmine-types'; @@ -74,6 +75,19 @@ export class FlatIssuesStore { return; } + async enhanceIssues(enhancers: IssueEnhancerInterface[]): Promise { + for (const issueId in this.issues) { + if (Object.prototype.hasOwnProperty.call(this.issues, issueId)) { + let issue = this.issues[issueId]; + for (let i = 0; i < enhancers.length; i++) { + const enhancer = enhancers[i]; + issue = await enhancer.enhance(issue); + this.issues[issueId] = issue; + } + } + } + } + getIds(): number[] { return Object.keys(this.issues).map((i) => Number(i)); } diff --git a/libs/event-emitter/src/utils/tree-issues-store.ts b/libs/event-emitter/src/utils/tree-issues-store.ts index 9f6cffe..ce2bf0d 100644 --- a/libs/event-emitter/src/utils/tree-issues-store.ts +++ b/libs/event-emitter/src/utils/tree-issues-store.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-namespace */ +import { IssueEnhancerInterface } from '../issue-enhancers/issue-enhancer-interface'; import { RedmineTypes } from '../models/redmine-types'; import { FlatIssuesStore, FlatIssuesStoreNs } from './flat-issues-store'; @@ -28,6 +29,10 @@ export class TreeIssuesStore { await this.flatStore.fillData(loader); } + async enhanceIssues(enhancers: IssueEnhancerInterface[]): Promise { + await this.flatStore.enhanceIssues(enhancers); + } + getFlatStore(): FlatIssuesStore { return this.flatStore; } diff --git a/views/simple-kanban-board.hbs b/views/simple-kanban-board.hbs index 8688ffb..93f7ccd 100644 --- a/views/simple-kanban-board.hbs +++ b/views/simple-kanban-board.hbs @@ -40,6 +40,28 @@ .kanban-card .kanban-card-title { font-weight: bold; } + .timepassed-dot { + height: 10px; + width: 10px; + background-color: #bbb; + border-radius: 50%; + display: inline-block; + } + .timepassed-dot.hot { + background-color: red; + } + .timepassed-dot.warm { + background-color: orange; + } + .timepassed-dot.comfort { + background-color: rgba(255, 255, 0, 0.4); + } + .timepassed-dot.breezy { + background-color: rgba(0, 255, 0, 0.4); + } + .timepassed-dot.cold { + background-color: rgba(0, 0, 255, 0.1); + } @@ -52,7 +74,7 @@
{{this.status}}
{{#each this.issues}}
- +
Исп.: {{this.current_user.name}}
Прогресс: {{this.done_ration}}
Трудозатраты: {{this.total_spent_hours}} / {{this.total_estimated_hours}}