Добавлен цветовой индикатор по времени с последнего обновления
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 { 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],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue