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

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 { 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],
};

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 };
};
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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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();

View file

@ -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));
}

View file

@ -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;
}

View file

@ -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>