Merge branch 'dev' into feature/react3-with-mobx
This commit is contained in:
commit
92e56177d9
11 changed files with 521 additions and 3 deletions
|
|
@ -9,5 +9,10 @@
|
||||||
},
|
},
|
||||||
"telegramBotToken": "",
|
"telegramBotToken": "",
|
||||||
"personalMessageTemplate": "",
|
"personalMessageTemplate": "",
|
||||||
"periodValidityNotification": 43200 // 12h
|
"periodValidityNotification": 43200000, // 12h
|
||||||
|
"tagManager": {
|
||||||
|
"updateInterval": 15000,
|
||||||
|
"updateItemsLimit": 3,
|
||||||
|
"tagsCustomFieldName": "Tags"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import { DynamicModule, Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||||
import { EventEmitterService } from './event-emitter.service';
|
import { EventEmitterService } from './event-emitter.service';
|
||||||
import { RedmineEventsGateway } from './events/redmine-events.gateway';
|
import { RedmineEventsGateway } from './events/redmine-events.gateway';
|
||||||
import MainConfig from './configs/main-config';
|
import MainConfig from './configs/main-config';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { RedmineDataLoader } from './redmine-data-loader/redmine-data-loader';
|
import { RedmineDataLoader } from './redmine-data-loader/redmine-data-loader';
|
||||||
import { MainController } from './main/main.controller';
|
import { MainController } from './main/main.controller';
|
||||||
import { ModuleParams } from './models/module-params';
|
import { ModuleParams } from './models/module-params';
|
||||||
|
|
@ -27,6 +27,7 @@ import { ListIssuesByUsersWidgetService } from './project-dashboard/widgets/list
|
||||||
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';
|
import { TimePassedHighlightEnhancer } from './issue-enhancers/time-passed-highlight-enhancer';
|
||||||
import { ListIssuesByFieldsWidgetService } from './project-dashboard/widgets/list-issues-by-fields.widget.service';
|
import { ListIssuesByFieldsWidgetService } from './project-dashboard/widgets/list-issues-by-fields.widget.service';
|
||||||
|
import { IssuesUpdaterService } from './issues-updater/issues-updater.service';
|
||||||
|
|
||||||
@Module({})
|
@Module({})
|
||||||
export class EventEmitterModule implements OnModuleInit {
|
export class EventEmitterModule implements OnModuleInit {
|
||||||
|
|
@ -58,6 +59,15 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
ListIssuesByUsersLikeJiraWidgetService,
|
ListIssuesByUsersLikeJiraWidgetService,
|
||||||
TimePassedHighlightEnhancer,
|
TimePassedHighlightEnhancer,
|
||||||
ListIssuesByFieldsWidgetService,
|
ListIssuesByFieldsWidgetService,
|
||||||
|
{
|
||||||
|
provide: 'ISSUES_UPDATER_SERVICE',
|
||||||
|
useFactory: (configService: ConfigService) => {
|
||||||
|
const redminePublicUrl =
|
||||||
|
configService.get<string>('redmineUrlPublic');
|
||||||
|
return new IssuesUpdaterService(redminePublicUrl);
|
||||||
|
},
|
||||||
|
inject: [ConfigService],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
EventEmitterService,
|
EventEmitterService,
|
||||||
|
|
@ -81,6 +91,10 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
ListIssuesByUsersLikeJiraWidgetService,
|
ListIssuesByUsersLikeJiraWidgetService,
|
||||||
TimePassedHighlightEnhancer,
|
TimePassedHighlightEnhancer,
|
||||||
ListIssuesByFieldsWidgetService,
|
ListIssuesByFieldsWidgetService,
|
||||||
|
{
|
||||||
|
provide: 'ISSUES_UPDATER_SERVICE',
|
||||||
|
useExisting: 'ISSUES_UPDATER_SERVICE',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
controllers: [MainController, UsersController, IssuesController],
|
controllers: [MainController, UsersController, IssuesController],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
class SimpleIssueUpdater {
|
||||||
|
constructor(
|
||||||
|
private userApiKey: string,
|
||||||
|
private updater: IssuesUpdaterService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async updateIssue(
|
||||||
|
issueId: number,
|
||||||
|
issue: Record<string, any>,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return await this.updater.updateIssue(issueId, issue, this.userApiKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IssuesUpdaterService {
|
||||||
|
constructor(private redminePublicUrl: string) {}
|
||||||
|
|
||||||
|
createSimpleUpdater(userApiKey: string): SimpleIssueUpdater {
|
||||||
|
return new SimpleIssueUpdater(userApiKey, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateIssue(
|
||||||
|
issueId: number,
|
||||||
|
issue: Record<string, any>,
|
||||||
|
userApiKey: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const url = this.getUrl(issueId);
|
||||||
|
const data = { issue: issue };
|
||||||
|
const resp = await axios.put(url, data, {
|
||||||
|
headers: { 'X-Redmine-API-Key': userApiKey },
|
||||||
|
});
|
||||||
|
return Boolean(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUrl(issueId: number): string {
|
||||||
|
return `${this.redminePublicUrl}/issues/${issueId}.json`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,8 @@ export class Queue<T, NT> {
|
||||||
|
|
||||||
queue: Subject<NT[]> = new Subject<NT[]>();
|
queue: Subject<NT[]> = new Subject<NT[]>();
|
||||||
|
|
||||||
|
finished: Subject<boolean> = new Subject();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private updateInterval: number,
|
private updateInterval: number,
|
||||||
private itemsLimit: number,
|
private itemsLimit: number,
|
||||||
|
|
@ -43,6 +45,9 @@ export class Queue<T, NT> {
|
||||||
const items = this.items.splice(0, this.itemsLimit);
|
const items = this.items.splice(0, this.itemsLimit);
|
||||||
const transformedItems = await this.transformationFn(items);
|
const transformedItems = await this.transformationFn(items);
|
||||||
this.queue.next(transformedItems);
|
this.queue.next(transformedItems);
|
||||||
|
if (this.items.length <= 0) {
|
||||||
|
this.finished.next(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.updateTimeout = setTimeout(() => {
|
this.updateTimeout = setTimeout(() => {
|
||||||
this.update();
|
this.update();
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ import { IssuesByTagsWidgetService } from './dashboards/widgets/issues-by-tags.w
|
||||||
import { CategoryMergeToTagsEnhancer } from './issue-enhancers/category-merge-to-tags-enhancer';
|
import { CategoryMergeToTagsEnhancer } from './issue-enhancers/category-merge-to-tags-enhancer';
|
||||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { SimpleIssuesListController } from './dashboards/simple-issues-list.controller';
|
||||||
|
import { TagsManagerController } from './tags-manager/tags-manager.controller';
|
||||||
|
import { CreateTagManagerServiceProvider } from './tags-manager/tags-manager.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -68,6 +71,8 @@ import { join } from 'path';
|
||||||
CurrentIssuesEccmReportController,
|
CurrentIssuesEccmReportController,
|
||||||
DailyEccmReportController,
|
DailyEccmReportController,
|
||||||
SimpleKanbanBoardController,
|
SimpleKanbanBoardController,
|
||||||
|
SimpleIssuesListController,
|
||||||
|
TagsManagerController,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AppService,
|
AppService,
|
||||||
|
|
@ -103,6 +108,7 @@ import { join } from 'path';
|
||||||
},
|
},
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
},
|
},
|
||||||
|
CreateTagManagerServiceProvider('TAG_MANAGER_SERVICE'),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements OnModuleInit {
|
export class AppModule implements OnModuleInit {
|
||||||
|
|
|
||||||
34
src/dashboards/simple-issues-list.controller.ts
Normal file
34
src/dashboards/simple-issues-list.controller.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader';
|
||||||
|
import { Controller, Get, Param, Render } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { IssuesByTagsWidgetService } from './widgets/issues-by-tags.widget.service';
|
||||||
|
import { parse } from 'jsonc-parser';
|
||||||
|
|
||||||
|
@Controller('simple-issues-list')
|
||||||
|
export class SimpleIssuesListController {
|
||||||
|
private path: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private issuesByTagsWidgetService: IssuesByTagsWidgetService,
|
||||||
|
private dynamicLoader: DynamicLoader,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {
|
||||||
|
this.path = this.configService.get<string>('simpleKanbanBoard.path');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/by-tags/:name/raw')
|
||||||
|
async getByTagsRawData(@Param('name') name: string): Promise<any> {
|
||||||
|
const cfg = this.dynamicLoader.load(name, {
|
||||||
|
path: this.path,
|
||||||
|
ext: 'jsonc',
|
||||||
|
parser: parse,
|
||||||
|
});
|
||||||
|
return await this.issuesByTagsWidgetService.render(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/by-tags/:name')
|
||||||
|
@Render('simple-issues-list')
|
||||||
|
async getByTags(@Param('name') name: string): Promise<any> {
|
||||||
|
return await this.getByTagsRawData(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,4 +21,9 @@ export type AppConfig = {
|
||||||
};
|
};
|
||||||
telegramBotToken: string;
|
telegramBotToken: string;
|
||||||
periodValidityNotification: number;
|
periodValidityNotification: number;
|
||||||
|
tagManager: {
|
||||||
|
updateInterval: number;
|
||||||
|
updateItemsLimit: number;
|
||||||
|
tagsCustomFieldName: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
23
src/tags-manager/tags-manager.controller.ts
Normal file
23
src/tags-manager/tags-manager.controller.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Body, Controller, Inject, Post } from '@nestjs/common';
|
||||||
|
import { TagsManagerService, UpdateRule } from './tags-manager.service';
|
||||||
|
|
||||||
|
type UpdateParams = {
|
||||||
|
userApiKey: string;
|
||||||
|
updateRules: UpdateRule[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@Controller('tags-manager')
|
||||||
|
export class TagsManagerController {
|
||||||
|
constructor(
|
||||||
|
@Inject('TAG_MANAGER_SERVICE')
|
||||||
|
private tagsManagerService: TagsManagerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('/update')
|
||||||
|
async update(@Body() params: UpdateParams): Promise<any> {
|
||||||
|
return await this.tagsManagerService.updateTags(
|
||||||
|
params.userApiKey,
|
||||||
|
params.updateRules,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
268
src/tags-manager/tags-manager.service.ts
Normal file
268
src/tags-manager/tags-manager.service.ts
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
import { IssuesUpdaterService } from '@app/event-emitter/issues-updater/issues-updater.service';
|
||||||
|
import { IssuesService } from '@app/event-emitter/issues/issues.service';
|
||||||
|
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
||||||
|
import { Queue } from '@app/event-emitter/queue/queue';
|
||||||
|
import { FlatIssuesStore } from '@app/event-emitter/utils/flat-issues-store';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
export type UpdateRule = {
|
||||||
|
issueIds: number[];
|
||||||
|
add: string[];
|
||||||
|
remove: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdateRuleSingleIssue = {
|
||||||
|
issueId: number;
|
||||||
|
issueData: RedmineTypes.Issue | null;
|
||||||
|
userApiKey: string;
|
||||||
|
add: string[];
|
||||||
|
remove: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModifyResult = {
|
||||||
|
value: string;
|
||||||
|
modified: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModifyCustomField = {
|
||||||
|
field?: RedmineTypes.CustomField;
|
||||||
|
modified?: boolean;
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdateResult = {
|
||||||
|
issue?: RedmineTypes.Issue | null;
|
||||||
|
delta?: Record<string, any>;
|
||||||
|
modified?: boolean;
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CreateTagManagerServiceProvider(providerName: string): any {
|
||||||
|
return {
|
||||||
|
provide: providerName,
|
||||||
|
useFactory: (
|
||||||
|
configService: ConfigService,
|
||||||
|
issuesUpdaterService: IssuesUpdaterService,
|
||||||
|
issuesService: IssuesService,
|
||||||
|
) => {
|
||||||
|
const updateInverval = configService.get<number>(
|
||||||
|
'tagManager.updateInverval',
|
||||||
|
);
|
||||||
|
const updateItemsLimit = configService.get<number>(
|
||||||
|
'tagManager.updateItemsLimit',
|
||||||
|
);
|
||||||
|
const tagsCustomFieldName = configService.get<string>(
|
||||||
|
'tagManager.tagsCustomFieldName',
|
||||||
|
);
|
||||||
|
return new TagsManagerService(
|
||||||
|
issuesUpdaterService,
|
||||||
|
updateInverval,
|
||||||
|
updateItemsLimit,
|
||||||
|
tagsCustomFieldName,
|
||||||
|
issuesService,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
inject: [ConfigService, 'ISSUES_UPDATER_SERVICE', IssuesService],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TagsManagerService {
|
||||||
|
private logger = new Logger(TagsManagerService.name);
|
||||||
|
private queue: Queue<UpdateRuleSingleIssue, UpdateResult>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private issuesUpdaterService: IssuesUpdaterService,
|
||||||
|
private updateInterval: number,
|
||||||
|
private updateItemsLimit: number,
|
||||||
|
private tagsCustomFieldName: string,
|
||||||
|
private issuesService: IssuesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async updateTags(
|
||||||
|
userApiKey: string,
|
||||||
|
updateRules: UpdateRule[],
|
||||||
|
): Promise<void> {
|
||||||
|
this.logger.debug(`Params for tags updates - ${updateRules}`);
|
||||||
|
const rules = await this.createUpdateRulesSingleIssue(
|
||||||
|
userApiKey,
|
||||||
|
updateRules,
|
||||||
|
);
|
||||||
|
const queue = this.getQueue();
|
||||||
|
queue.add(rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQueue(): Queue<UpdateRuleSingleIssue, UpdateResult> {
|
||||||
|
if (!this.queue) {
|
||||||
|
this.queue = new Queue<UpdateRuleSingleIssue, UpdateResult>(
|
||||||
|
this.updateInterval,
|
||||||
|
this.updateItemsLimit,
|
||||||
|
async (rules: UpdateRuleSingleIssue[]) => {
|
||||||
|
const results = [] as UpdateResult[];
|
||||||
|
for (let i = 0; i < rules.length; i++) {
|
||||||
|
const rule = rules[i];
|
||||||
|
let result: UpdateResult;
|
||||||
|
try {
|
||||||
|
result = await this.updateIssue(rule);
|
||||||
|
} catch (ex) {
|
||||||
|
this.logger.error(
|
||||||
|
`Error at execution update task for issueId = ${rule.issueId}, ` +
|
||||||
|
`add = ${JSON.stringify(rule.add)}, ` +
|
||||||
|
`remove = ${JSON.stringify(rule.remove)}, ` +
|
||||||
|
`error = ${ex.message}`,
|
||||||
|
);
|
||||||
|
result = { success: false };
|
||||||
|
}
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.queue.start();
|
||||||
|
}
|
||||||
|
return this.queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateIssue(
|
||||||
|
rule: UpdateRuleSingleIssue,
|
||||||
|
): Promise<UpdateResult> {
|
||||||
|
if (!rule.issueData) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (rule.add.length == 0 && rule.remove.length == 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
delta: {},
|
||||||
|
issue: rule.issueData,
|
||||||
|
modified: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const result = this.modifyTagsForIssue(rule);
|
||||||
|
if (result.modified) {
|
||||||
|
const delta = { custom_fields: [result.field] };
|
||||||
|
const updateResult = await this.issuesUpdaterService.updateIssue(
|
||||||
|
rule.issueId,
|
||||||
|
delta,
|
||||||
|
rule.userApiKey,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: updateResult,
|
||||||
|
delta: delta,
|
||||||
|
issue: rule.issueData,
|
||||||
|
modified: result.modified,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
delta: {},
|
||||||
|
issue: rule.issueData,
|
||||||
|
modified: result.modified,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createIssuesStore(
|
||||||
|
updateRules: UpdateRule[],
|
||||||
|
): Promise<FlatIssuesStore> {
|
||||||
|
const issuesStore = new FlatIssuesStore();
|
||||||
|
for (let i = 0; i < updateRules.length; i++) {
|
||||||
|
const updateRule = updateRules[i];
|
||||||
|
for (let j = 0; j < updateRule.issueIds.length; j++) {
|
||||||
|
const issueId = updateRule.issueIds[j];
|
||||||
|
issuesStore.push(issueId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loader = this.issuesService.createDynamicIssuesLoader();
|
||||||
|
await issuesStore.fillData(loader);
|
||||||
|
|
||||||
|
return issuesStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTagsCustomField(
|
||||||
|
issue: RedmineTypes.Issue,
|
||||||
|
): RedmineTypes.CustomField | null {
|
||||||
|
if (!issue.custom_fields) return null;
|
||||||
|
const customFields = issue.custom_fields;
|
||||||
|
return (
|
||||||
|
customFields.find((cf) => cf.name == this.tagsCustomFieldName) || null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTags(tags: string): string[] {
|
||||||
|
return tags
|
||||||
|
.split(/[ ,;]/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => !!s);
|
||||||
|
}
|
||||||
|
|
||||||
|
private modifyTags(
|
||||||
|
tags: string,
|
||||||
|
updateRule: UpdateRuleSingleIssue,
|
||||||
|
): ModifyResult {
|
||||||
|
const t = this.getTags(tags);
|
||||||
|
let modified = false;
|
||||||
|
for (let i = 0; i < updateRule.remove.length; i++) {
|
||||||
|
const tagForRemoving = updateRule.remove[i];
|
||||||
|
const tagIndex = t.indexOf(tagForRemoving);
|
||||||
|
if (tagIndex >= 0) {
|
||||||
|
t.splice(tagIndex, 1);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < updateRule.add.length; i++) {
|
||||||
|
const tagForAdding = updateRule.add[i];
|
||||||
|
const tagIndex = t.indexOf(tagForAdding);
|
||||||
|
if (tagIndex < 0) {
|
||||||
|
t.push(tagForAdding);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = modified ? t.join(' ') : tags;
|
||||||
|
return { value: result, modified: modified };
|
||||||
|
}
|
||||||
|
|
||||||
|
private modifyTagsForIssue(
|
||||||
|
updateRule: UpdateRuleSingleIssue,
|
||||||
|
): ModifyCustomField {
|
||||||
|
const cf = this.getTagsCustomField(updateRule.issueData);
|
||||||
|
if (!cf) return { success: false };
|
||||||
|
const modifyResult = this.modifyTags(cf.value, updateRule);
|
||||||
|
if (modifyResult.modified) cf.value = modifyResult.value;
|
||||||
|
return { success: true, modified: modifyResult.modified, field: cf };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createUpdateRulesSingleIssue(
|
||||||
|
userApiKey: string,
|
||||||
|
items: UpdateRule[],
|
||||||
|
): Promise<UpdateRuleSingleIssue[]> {
|
||||||
|
const res = [] as UpdateRuleSingleIssue[];
|
||||||
|
const store = await this.createIssuesStore(items);
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i];
|
||||||
|
for (let j = 0; j < item.issueIds.length; j++) {
|
||||||
|
const issueId = item.issueIds[j];
|
||||||
|
const issue = this.getIssue(store, issueId);
|
||||||
|
res.push({
|
||||||
|
issueId: issueId,
|
||||||
|
issueData: issue,
|
||||||
|
userApiKey: userApiKey,
|
||||||
|
add: item.add,
|
||||||
|
remove: item.remove,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIssue(
|
||||||
|
store: FlatIssuesStore,
|
||||||
|
id: number,
|
||||||
|
): RedmineTypes.Issue | null {
|
||||||
|
const result = store.getIssue(id);
|
||||||
|
return result.data ? result.data : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
views/simple-issues-list.hbs
Normal file
114
views/simple-issues-list.hbs
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Simple Issues List</title>
|
||||||
|
<style>
|
||||||
|
.list-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
background-color: rgb(225, 225, 225);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
margin: 5px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item-description {
|
||||||
|
background-color: rgb(196, 196, 196);
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: rgba(255, 255, 255, 0.2) rgba(96, 96, 96, 0.2) rgba(96, 96, 96, 0.2) rgba(255, 255, 255, 0.2);
|
||||||
|
border-style: solid;
|
||||||
|
margin: 2px;
|
||||||
|
padding: 3px;
|
||||||
|
width: 190px;
|
||||||
|
/*display: flex;*/
|
||||||
|
border-radius: 3px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-card div {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue-tag {
|
||||||
|
font-size: 8pt;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container {
|
||||||
|
padding-left: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{{#each this}}
|
||||||
|
{{#if this.metainfo}}
|
||||||
|
<h1 id="{{this.metainfo.title}}">{{this.metainfo.title}} <a href="#{{this.metainfo.title}}">#</a></h1>
|
||||||
|
<div class="list-container">
|
||||||
|
{{#each this.data}}
|
||||||
|
|
||||||
|
{{#each this.issues}}
|
||||||
|
<div class="list-item">
|
||||||
|
<div>
|
||||||
|
<span class="timepassed-dot {{this.timePassedClass}}"></span>
|
||||||
|
<span class="issue-subject"><a href="{{{this.url.url}}}">{{this.tracker.name}} #{{this.id}} - {{this.subject}}</a></span>
|
||||||
|
<span class="issue-status">| {{this.status.name}}</span>
|
||||||
|
<span class="issue-time">| {{this.total_spent_hours}} / {{this.total_estimated_hours}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tags-container">
|
||||||
|
{{#if this.styledTags}}
|
||||||
|
{{#each this.styledTags}}
|
||||||
|
<span class="issue-tag" style="{{{this.style}}}">{{this.tag}}</span>
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -82,7 +82,9 @@
|
||||||
<div class="kanban-card">
|
<div class="kanban-card">
|
||||||
<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 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.priority.name}}</div>
|
||||||
|
<div>Версия: {{this.fixed_version.name}}</div>
|
||||||
|
<div>Прогресс: {{this.done_ratio}}</div>
|
||||||
<div>Трудозатраты: {{this.total_spent_hours}} / {{this.total_estimated_hours}}</div>
|
<div>Трудозатраты: {{this.total_spent_hours}} / {{this.total_estimated_hours}}</div>
|
||||||
{{#if this.styledTags}}
|
{{#if this.styledTags}}
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue