Добавлен цветовой индикатор по времени с последнего обновления

This commit is contained in:
Pavel Gnedov 2023-02-13 21:09:36 +07:00
parent 1f3964f5ca
commit 2aac9ae94c
9 changed files with 135 additions and 4 deletions

View file

@ -25,6 +25,7 @@ import { RedminePublicUrlConverter } from './converters/redmine-public-url.conve
import { IssueUrlEnhancer } from './issue-enhancers/issue-url-enhancer'; import { IssueUrlEnhancer } from './issue-enhancers/issue-url-enhancer';
import { ListIssuesByUsersWidgetService } from './project-dashboard/widgets/list-issues-by-users.widget.service'; 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 { ListIssuesByUsersLikeJiraWidgetService } from './project-dashboard/widgets/list-issues-by-users-like-jira.widget.service';
import { TimePassedHighlightEnhancer } from './issue-enhancers/time-passed-highlight-enhancer';
@Module({}) @Module({})
export class EventEmitterModule implements OnModuleInit { export class EventEmitterModule implements OnModuleInit {
@ -54,6 +55,7 @@ export class EventEmitterModule implements OnModuleInit {
IssueUrlEnhancer, IssueUrlEnhancer,
ListIssuesByUsersWidgetService, ListIssuesByUsersWidgetService,
ListIssuesByUsersLikeJiraWidgetService, ListIssuesByUsersLikeJiraWidgetService,
TimePassedHighlightEnhancer,
], ],
exports: [ exports: [
EventEmitterService, EventEmitterService,
@ -75,6 +77,7 @@ export class EventEmitterModule implements OnModuleInit {
IssueUrlEnhancer, IssueUrlEnhancer,
ListIssuesByUsersWidgetService, ListIssuesByUsersWidgetService,
ListIssuesByUsersLikeJiraWidgetService, ListIssuesByUsersLikeJiraWidgetService,
TimePassedHighlightEnhancer,
], ],
controllers: [MainController, UsersController, IssuesController], controllers: [MainController, UsersController, IssuesController],
}; };

View file

@ -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<RedmineTypes.ExtendedIssue> {
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;
}
}

View file

@ -61,6 +61,8 @@ export module RedmineTypes {
parent?: { id: number }; parent?: { id: number };
}; };
export type ExtendedIssue = Issue & Record<string, any>;
// eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace
export module Unknown { export module Unknown {
export const num = -1; export const num = -1;

View file

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-namespace */
import { TimePassedHighlightEnhancer } from '@app/event-emitter/issue-enhancers/time-passed-highlight-enhancer';
import { import {
IssuesService, IssuesService,
IssuesServiceNs, IssuesServiceNs,
@ -31,7 +32,10 @@ export class ListIssuesByUsersLikeJiraWidgetService
private logger = new Logger(ListIssuesByUsersLikeJiraWidgetService.name); private logger = new Logger(ListIssuesByUsersLikeJiraWidgetService.name);
private issuesLoader: IssuesServiceNs.IssuesLoader; private issuesLoader: IssuesServiceNs.IssuesLoader;
constructor(private issuesService: IssuesService) { constructor(
private issuesService: IssuesService,
private timePassedHighlightEnhancer: TimePassedHighlightEnhancer,
) {
this.issuesLoader = this.issuesService.createDynamicIssuesLoader(); this.issuesLoader = this.issuesService.createDynamicIssuesLoader();
} }
@ -86,6 +90,7 @@ export class ListIssuesByUsersLikeJiraWidgetService
const rootIssue = await this.issuesService.getIssue(issueId); const rootIssue = await this.issuesService.getIssue(issueId);
treeStore.setRootIssue(rootIssue); treeStore.setRootIssue(rootIssue);
await treeStore.fillData(this.issuesLoader); await treeStore.fillData(this.issuesLoader);
await treeStore.enhanceIssues([this.timePassedHighlightEnhancer]);
return treeStore.getFlatStore(); return treeStore.getFlatStore();
} }
@ -98,6 +103,7 @@ export class ListIssuesByUsersLikeJiraWidgetService
const issue = rawData[i]; const issue = rawData[i];
store.push(issue); store.push(issue);
} }
await store.enhanceIssues([this.timePassedHighlightEnhancer]);
return store; return store;
} }

View file

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-namespace */
import { TimePassedHighlightEnhancer } from '@app/event-emitter/issue-enhancers/time-passed-highlight-enhancer';
import { import {
IssuesService, IssuesService,
IssuesServiceNs, IssuesServiceNs,
@ -43,7 +44,10 @@ export class ListIssuesByUsersWidgetService
private logger = new Logger(ListIssuesByUsersWidgetService.name); private logger = new Logger(ListIssuesByUsersWidgetService.name);
private issuesLoader: IssuesServiceNs.IssuesLoader; private issuesLoader: IssuesServiceNs.IssuesLoader;
constructor(private issuesService: IssuesService) { constructor(
private issuesService: IssuesService,
private timePassedHighlightEnhancer: TimePassedHighlightEnhancer,
) {
this.issuesLoader = this.issuesService.createDynamicIssuesLoader(); this.issuesLoader = this.issuesService.createDynamicIssuesLoader();
} }
@ -89,6 +93,7 @@ export class ListIssuesByUsersWidgetService
const rootIssue = await this.issuesService.getIssue(issueId); const rootIssue = await this.issuesService.getIssue(issueId);
treeStore.setRootIssue(rootIssue); treeStore.setRootIssue(rootIssue);
await treeStore.fillData(this.issuesLoader); await treeStore.fillData(this.issuesLoader);
await treeStore.enhanceIssues([this.timePassedHighlightEnhancer]);
return treeStore.getFlatStore(); return treeStore.getFlatStore();
} }
@ -101,6 +106,7 @@ export class ListIssuesByUsersWidgetService
const issue = rawData[i]; const issue = rawData[i];
store.push(issue); store.push(issue);
} }
await store.enhanceIssues([this.timePassedHighlightEnhancer]);
return store; return store;
} }

View file

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-namespace */
import { TimePassedHighlightEnhancer } from '@app/event-emitter/issue-enhancers/time-passed-highlight-enhancer';
import { import {
IssuesService, IssuesService,
IssuesServiceNs, IssuesServiceNs,
@ -40,7 +41,10 @@ export class RootIssueSubTreesWidgetService
{ {
private issuesLoader: IssuesServiceNs.IssuesLoader; private issuesLoader: IssuesServiceNs.IssuesLoader;
constructor(private issuesService: IssuesService) { constructor(
private issuesService: IssuesService,
private timePassedHighlightEnhancer: TimePassedHighlightEnhancer,
) {
this.issuesLoader = this.issuesService.createDynamicIssuesLoader(); this.issuesLoader = this.issuesService.createDynamicIssuesLoader();
} }
@ -55,6 +59,7 @@ export class RootIssueSubTreesWidgetService
); );
treeStore.setRootIssue(rootIssue); treeStore.setRootIssue(rootIssue);
await treeStore.fillData(this.issuesLoader); await treeStore.fillData(this.issuesLoader);
await treeStore.enhanceIssues([this.timePassedHighlightEnhancer]);
let stories: TreeIssuesStoreNs.Models.GetFlatStories.Result; let stories: TreeIssuesStoreNs.Models.GetFlatStories.Result;
if (widgetParams.parentsAsGroups) { if (widgetParams.parentsAsGroups) {
stories = treeStore.getFlatStoriesByParents(); stories = treeStore.getFlatStoriesByParents();

View file

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-namespace */
import { IssueEnhancerInterface } from '../issue-enhancers/issue-enhancer-interface';
import { IssuesServiceNs } from '../issues/issues.service'; import { IssuesServiceNs } from '../issues/issues.service';
import { RedmineTypes } from '../models/redmine-types'; import { RedmineTypes } from '../models/redmine-types';
@ -74,6 +75,19 @@ export class FlatIssuesStore {
return; return;
} }
async enhanceIssues(enhancers: IssueEnhancerInterface[]): Promise<void> {
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[] { getIds(): number[] {
return Object.keys(this.issues).map((i) => Number(i)); return Object.keys(this.issues).map((i) => Number(i));
} }

View file

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-namespace */
import { IssueEnhancerInterface } from '../issue-enhancers/issue-enhancer-interface';
import { RedmineTypes } from '../models/redmine-types'; import { RedmineTypes } from '../models/redmine-types';
import { FlatIssuesStore, FlatIssuesStoreNs } from './flat-issues-store'; import { FlatIssuesStore, FlatIssuesStoreNs } from './flat-issues-store';
@ -28,6 +29,10 @@ export class TreeIssuesStore {
await this.flatStore.fillData(loader); await this.flatStore.fillData(loader);
} }
async enhanceIssues(enhancers: IssueEnhancerInterface[]): Promise<void> {
await this.flatStore.enhanceIssues(enhancers);
}
getFlatStore(): FlatIssuesStore { getFlatStore(): FlatIssuesStore {
return this.flatStore; return this.flatStore;
} }

View file

@ -40,6 +40,28 @@
.kanban-card .kanban-card-title { .kanban-card .kanban-card-title {
font-weight: bold; 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);
}
</style> </style>
</head> </head>
@ -52,7 +74,7 @@
<div class="kanban-header">{{this.status}}</div> <div class="kanban-header">{{this.status}}</div>
{{#each this.issues}} {{#each this.issues}}
<div class="kanban-card"> <div class="kanban-card">
<div class="kanban-card-title"><a href="{{{this.url.url}}}">{{this.tracker.name}} #{{this.id}} - {{this.subject}}</a></div> <div class="kanban-card-title"><span class="timepassed-dot {{this.timePassedClass}}"></span> <a href="{{{this.url.url}}}">{{this.tracker.name}} #{{this.id}} - {{this.subject}}</a></div>
<div>Исп.: {{this.current_user.name}}</div> <div>Исп.: {{this.current_user.name}}</div>
<div>Прогресс: {{this.done_ration}}</div> <div>Прогресс: {{this.done_ration}}</div>
<div>Трудозатраты: {{this.total_spent_hours}} / {{this.total_estimated_hours}}</div> <div>Трудозатраты: {{this.total_spent_hours}} / {{this.total_estimated_hours}}</div>