Добавлен цветовой индикатор по времени с последнего обновления
This commit is contained in:
parent
1f3964f5ca
commit
2aac9ae94c
9 changed files with 135 additions and 4 deletions
|
|
@ -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],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,8 @@ export module RedmineTypes {
|
|||
parent?: { id: number };
|
||||
};
|
||||
|
||||
export type ExtendedIssue = Issue & Record<string, any>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace
|
||||
export module Unknown {
|
||||
export const num = -1;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<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[] {
|
||||
return Object.keys(this.issues).map((i) => Number(i));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
await this.flatStore.enhanceIssues(enhancers);
|
||||
}
|
||||
|
||||
getFlatStore(): FlatIssuesStore {
|
||||
return this.flatStore;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
@ -52,7 +74,7 @@
|
|||
<div class="kanban-header">{{this.status}}</div>
|
||||
{{#each this.issues}}
|
||||
<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.done_ration}}</div>
|
||||
<div>Трудозатраты: {{this.total_spent_hours}} / {{this.total_estimated_hours}}</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue