Рефакторинг - разделены провайдеры данных от виджетов с представлениями
This commit is contained in:
parent
c8e71d3926
commit
10b04bdb0f
21 changed files with 431 additions and 270 deletions
|
|
@ -1,36 +1,45 @@
|
||||||
import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common';
|
||||||
import { DashboardsService } from './dashboards.service';
|
import { DashboardsService } from './dashboards.service';
|
||||||
import { BadRequestErrorHandler, getOrAppErrorOrThrow } from '../utils/result';
|
import { BadRequestErrorHandler, getOrAppErrorOrThrow } from '../utils/result';
|
||||||
|
import { DashboardsDataService } from './dashboards-data.service';
|
||||||
|
|
||||||
@Controller('dashboard')
|
@Controller('dashboard')
|
||||||
export class DashboardController {
|
export class DashboardController {
|
||||||
constructor(private dashboardsService: DashboardsService) {}
|
constructor(
|
||||||
|
private dashboardsService: DashboardsService,
|
||||||
|
private dashboardsDataService: DashboardsDataService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post(':name')
|
@Post()
|
||||||
async create(@Param('name') name: string): Promise<string> {
|
async create(): Promise<string> {
|
||||||
if (name === 'anonymous') {
|
|
||||||
name = null;
|
|
||||||
}
|
|
||||||
const res = await getOrAppErrorOrThrow(
|
const res = await getOrAppErrorOrThrow(
|
||||||
() => this.dashboardsService.create(name),
|
() => this.dashboardsService.create(),
|
||||||
BadRequestErrorHandler,
|
BadRequestErrorHandler,
|
||||||
);
|
);
|
||||||
return res.id;
|
return res.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':name')
|
@Get(':id')
|
||||||
async load(@Param('name') name: string): Promise<any> {
|
async load(@Param('id') id: string): Promise<any> {
|
||||||
const res = await getOrAppErrorOrThrow(
|
const res = await getOrAppErrorOrThrow(
|
||||||
() => this.dashboardsService.load(name),
|
() => this.dashboardsService.load(id),
|
||||||
BadRequestErrorHandler,
|
BadRequestErrorHandler,
|
||||||
);
|
);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':name')
|
@Get(':id/load-data')
|
||||||
async save(@Param('name') name: string, @Body() data: any): Promise<void> {
|
async loadData(@Param('id') id: string): Promise<any> {
|
||||||
|
return await getOrAppErrorOrThrow(
|
||||||
|
() => this.dashboardsDataService.loadData(id),
|
||||||
|
BadRequestErrorHandler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
async save(@Param('id') id: string, @Body() data: any): Promise<void> {
|
||||||
const res = await getOrAppErrorOrThrow(
|
const res = await getOrAppErrorOrThrow(
|
||||||
() => this.dashboardsService.save(name, data),
|
() => this.dashboardsService.save(id, data),
|
||||||
BadRequestErrorHandler,
|
BadRequestErrorHandler,
|
||||||
);
|
);
|
||||||
return res;
|
return res;
|
||||||
|
|
|
||||||
67
libs/event-emitter/src/dashboards/dashboards-data.service.ts
Normal file
67
libs/event-emitter/src/dashboards/dashboards-data.service.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { DashboardsService } from './dashboards.service';
|
||||||
|
import * as DashboardModel from '../models/dashboard';
|
||||||
|
import {
|
||||||
|
AppError,
|
||||||
|
Result,
|
||||||
|
createAppError,
|
||||||
|
fail,
|
||||||
|
success,
|
||||||
|
} from '../utils/result';
|
||||||
|
import { WidgetsCollectionService } from './widgets-collection.service';
|
||||||
|
|
||||||
|
export type WidgetWithData = {
|
||||||
|
widget: DashboardModel.Widget;
|
||||||
|
data: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DashboardsDataService {
|
||||||
|
private logger = new Logger(DashboardsDataService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dashboardsService: DashboardsService,
|
||||||
|
private widgetsCollectionService: WidgetsCollectionService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async loadData(id: string): Promise<WidgetWithData[]> {
|
||||||
|
const cfg = await this.dashboardsService.load(id);
|
||||||
|
const results: WidgetWithData[] = [];
|
||||||
|
let isSuccess = false;
|
||||||
|
for (let i = 0; i < cfg.widgets.length; i++) {
|
||||||
|
const widget = cfg.widgets[i];
|
||||||
|
const loadRes = await this.loadWidgetData(
|
||||||
|
widget.type,
|
||||||
|
widget.widgetParams,
|
||||||
|
widget.dataLoaderParams,
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
if (loadRes.result) {
|
||||||
|
isSuccess = true;
|
||||||
|
results.push({
|
||||||
|
widget: widget,
|
||||||
|
data: loadRes.result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isSuccess) throw createAppError('CANNOT_LOAD_DATA');
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadWidgetData(
|
||||||
|
type: string,
|
||||||
|
widgetParams: DashboardModel.WidgetParams,
|
||||||
|
dataLoaderParams: DashboardModel.DataLoaderParams,
|
||||||
|
dashboardParams: DashboardModel.Data,
|
||||||
|
): Promise<Result<any, AppError>> {
|
||||||
|
const widgetResult = this.widgetsCollectionService.getWidgetByType(type);
|
||||||
|
if (widgetResult.error) return fail(createAppError(widgetResult.error));
|
||||||
|
const widget = widgetResult.result;
|
||||||
|
const renderResult = await widget.render(
|
||||||
|
widgetParams,
|
||||||
|
dataLoaderParams,
|
||||||
|
dashboardParams,
|
||||||
|
);
|
||||||
|
return renderResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Dashboards as DashboardsDb } from '../couchdb-datasources/dashboards';
|
import { Dashboards as DashboardsDb } from '../couchdb-datasources/dashboards';
|
||||||
import {
|
import * as DashboardModel from '../models/dashboard';
|
||||||
Dashboard as DashboardModel,
|
|
||||||
Data as DashboardData,
|
|
||||||
} from '../models/dashboard';
|
|
||||||
import nano from 'nano';
|
import nano from 'nano';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { createAppError } from '../utils/result';
|
import { createAppError } from '../utils/result';
|
||||||
|
|
@ -14,58 +11,56 @@ export class DashboardsService {
|
||||||
|
|
||||||
constructor(private db: DashboardsDb) {}
|
constructor(private db: DashboardsDb) {}
|
||||||
|
|
||||||
async create(name?: string): Promise<DashboardModel> {
|
async create(): Promise<DashboardModel.Dashboard> {
|
||||||
if (!name) {
|
const id = randomUUID();
|
||||||
name = randomUUID();
|
this.logger.debug(`Create new dashboard with id - ${id}`);
|
||||||
}
|
if (await this.isExists(id)) {
|
||||||
this.logger.debug(`Create new dashboard with name - ${name}`);
|
|
||||||
if (await this.isExists(name)) {
|
|
||||||
const err = createAppError('ALREADY_EXISTS');
|
const err = createAppError('ALREADY_EXISTS');
|
||||||
this.logger.error(`Error - ${JSON.stringify(err)}`);
|
this.logger.error(`Error - ${JSON.stringify(err)}`);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
const ds = await this.db.getDatasource();
|
const ds = await this.db.getDatasource();
|
||||||
const doc: nano.MaybeDocument & DashboardModel = {
|
const doc: nano.MaybeDocument & DashboardModel.Dashboard = {
|
||||||
_id: name,
|
_id: id,
|
||||||
id: name,
|
id: id,
|
||||||
data: null,
|
data: null,
|
||||||
};
|
};
|
||||||
await ds.insert(doc);
|
await ds.insert(doc);
|
||||||
return await ds.get(name);
|
return await ds.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadRawData(
|
async loadRawData(
|
||||||
name: string,
|
id: string,
|
||||||
): Promise<DashboardModel & nano.MaybeDocument> {
|
): Promise<DashboardModel.Dashboard & nano.MaybeDocument> {
|
||||||
this.logger.debug(`Load raw data, dashboard name - ${name}`);
|
this.logger.debug(`Load raw data, dashboard id - ${id}`);
|
||||||
const ds = await this.db.getDatasource();
|
const ds = await this.db.getDatasource();
|
||||||
if (!(await this.isExists(name))) throw createAppError('NOT_EXISTS');
|
if (!(await this.isExists(id))) throw createAppError('NOT_EXISTS');
|
||||||
const res = await ds.get(name);
|
const res = await ds.get(id);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(name: string): Promise<DashboardData> {
|
async load(id: string): Promise<DashboardModel.Data> {
|
||||||
this.logger.debug(`Load dashboard name - ${name}`);
|
this.logger.debug(`Load dashboard id - ${id}`);
|
||||||
const rawData = await this.loadRawData(name);
|
const rawData = await this.loadRawData(id);
|
||||||
return rawData.data;
|
return rawData.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async isExists(name: string): Promise<boolean> {
|
async isExists(id: string): Promise<boolean> {
|
||||||
const ds = await this.db.getDatasource();
|
const ds = await this.db.getDatasource();
|
||||||
try {
|
try {
|
||||||
await ds.get(name);
|
await ds.get(id);
|
||||||
return true;
|
return true;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(name: string, data: any): Promise<void> {
|
async save(id: string, data: DashboardModel.Data): Promise<void> {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Save dashboard name - ${name}, data - ${JSON.stringify(data)}`,
|
`Save dashboard id - ${id}, data - ${JSON.stringify(data)}`,
|
||||||
);
|
);
|
||||||
const ds = await this.db.getDatasource();
|
const ds = await this.db.getDatasource();
|
||||||
const prevValue = await this.loadRawData(name);
|
const prevValue = await this.loadRawData(id);
|
||||||
|
|
||||||
const newValue = {
|
const newValue = {
|
||||||
_id: prevValue._id,
|
_id: prevValue._id,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Result, AppError } from '../utils/result';
|
||||||
|
import { WidgetDataLoaderInterface } from './widget-data-loader-interface';
|
||||||
|
import { WidgetInterface } from './widget-interface';
|
||||||
|
|
||||||
|
export class InteractiveWidget
|
||||||
|
implements WidgetInterface<any, any, any, any, any>
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
public dataLoader: WidgetDataLoaderInterface<any, any, any>,
|
||||||
|
public type: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async render(
|
||||||
|
widgetParams: any,
|
||||||
|
dataLoaderParams: any,
|
||||||
|
dashboardParams: any,
|
||||||
|
): Promise<Result<any, AppError>> {
|
||||||
|
const data = await this.dataLoader.load(dataLoaderParams, dashboardParams);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInteractiveWidget(
|
||||||
|
dataLoader: WidgetDataLoaderInterface<any, any, any>,
|
||||||
|
type: string,
|
||||||
|
): WidgetInterface<any, any, any, any, any> {
|
||||||
|
return new InteractiveWidget(dataLoader, type);
|
||||||
|
}
|
||||||
35
libs/event-emitter/src/dashboards/text-widget-factory.ts
Normal file
35
libs/event-emitter/src/dashboards/text-widget-factory.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Result, AppError, success } from '../utils/result';
|
||||||
|
import { WidgetDataLoaderInterface } from './widget-data-loader-interface';
|
||||||
|
import { WidgetInterface } from './widget-interface';
|
||||||
|
import Handlebars from 'handlebars';
|
||||||
|
|
||||||
|
export class TextWidget implements WidgetInterface<any, any, any, any, any> {
|
||||||
|
constructor(
|
||||||
|
public dataLoader: WidgetDataLoaderInterface<any, any, any>,
|
||||||
|
public type: string,
|
||||||
|
public template: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async render(
|
||||||
|
widgetParams: any,
|
||||||
|
dataLoaderParams: any,
|
||||||
|
dashboardParams: any,
|
||||||
|
): Promise<Result<any, AppError>> {
|
||||||
|
const params = {
|
||||||
|
widgetParams,
|
||||||
|
dataLoaderParams,
|
||||||
|
dashboardParams,
|
||||||
|
};
|
||||||
|
const template = Handlebars.compile(this.template);
|
||||||
|
const res = template(params);
|
||||||
|
return success(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTextWidget(
|
||||||
|
dataLoader: WidgetDataLoaderInterface<any, any, any>,
|
||||||
|
type: string,
|
||||||
|
template: string,
|
||||||
|
): WidgetInterface<any, any, any, any, any> {
|
||||||
|
return new TextWidget(dataLoader, type, template);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { AppError, Result } from '../utils/result';
|
||||||
|
|
||||||
|
export interface WidgetDataLoaderInterface<DLP, DBP, R> {
|
||||||
|
isMyConfig(dataLoaderParams: DLP): boolean;
|
||||||
|
load(
|
||||||
|
dataLoaderParams: DLP,
|
||||||
|
dashboardParams: DBP,
|
||||||
|
): Promise<Result<R, AppError>>;
|
||||||
|
}
|
||||||
|
|
@ -12,8 +12,14 @@ import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from
|
||||||
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
|
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import nano from 'nano';
|
import nano from 'nano';
|
||||||
import { WidgetInterface } from '../widget-interface';
|
|
||||||
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
|
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
|
||||||
|
import { WidgetDataLoaderInterface } from '../widget-data-loader-interface';
|
||||||
|
import {
|
||||||
|
AppError,
|
||||||
|
Result,
|
||||||
|
createAppError,
|
||||||
|
success,
|
||||||
|
} from '@app/event-emitter/utils/result';
|
||||||
|
|
||||||
export namespace ListIssuesByFieldsWidgetNs {
|
export namespace ListIssuesByFieldsWidgetNs {
|
||||||
export type Params = {
|
export type Params = {
|
||||||
|
|
@ -34,10 +40,10 @@ export namespace ListIssuesByFieldsWidgetNs {
|
||||||
type Params = ListIssuesByFieldsWidgetNs.Params;
|
type Params = ListIssuesByFieldsWidgetNs.Params;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ListIssuesByFieldsWidgetService
|
export class ListIssuesByFieldsWidgetDataLoaderService
|
||||||
implements WidgetInterface<Params, any, any>
|
implements WidgetDataLoaderInterface<Params, any, any>
|
||||||
{
|
{
|
||||||
private logger = new Logger(ListIssuesByFieldsWidgetService.name);
|
private logger = new Logger(ListIssuesByFieldsWidgetDataLoaderService.name);
|
||||||
private issuesLoader: IssuesServiceNs.IssuesLoader;
|
private issuesLoader: IssuesServiceNs.IssuesLoader;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -52,7 +58,7 @@ export class ListIssuesByFieldsWidgetService
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(widgetParams: Params): Promise<any> {
|
async load(widgetParams: Params): Promise<Result<any, AppError>> {
|
||||||
let store: FlatIssuesStore;
|
let store: FlatIssuesStore;
|
||||||
if (widgetParams.fromRootIssueId) {
|
if (widgetParams.fromRootIssueId) {
|
||||||
store = await this.getListFromRoot(widgetParams.fromRootIssueId);
|
store = await this.getListFromRoot(widgetParams.fromRootIssueId);
|
||||||
|
|
@ -61,7 +67,7 @@ export class ListIssuesByFieldsWidgetService
|
||||||
} else {
|
} else {
|
||||||
const errMsg = `Wrong widgetParams value`;
|
const errMsg = `Wrong widgetParams value`;
|
||||||
this.logger.error(errMsg);
|
this.logger.error(errMsg);
|
||||||
throw new Error(errMsg);
|
return fail(createAppError(errMsg));
|
||||||
}
|
}
|
||||||
await store.enhanceIssues([
|
await store.enhanceIssues([
|
||||||
this.timePassedHighlightEnhancer,
|
this.timePassedHighlightEnhancer,
|
||||||
|
|
@ -87,7 +93,7 @@ export class ListIssuesByFieldsWidgetService
|
||||||
return a.metainfo.title.localeCompare(b.metainfo.title);
|
return a.metainfo.title.localeCompare(b.metainfo.title);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return res;
|
return success(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {
|
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {
|
||||||
|
|
@ -11,8 +11,14 @@ import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from
|
||||||
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
|
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import nano from 'nano';
|
import nano from 'nano';
|
||||||
import { WidgetInterface } from '../widget-interface';
|
|
||||||
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
|
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
|
||||||
|
import { WidgetDataLoaderInterface } from '../widget-data-loader-interface';
|
||||||
|
import {
|
||||||
|
AppError,
|
||||||
|
Result,
|
||||||
|
createAppError,
|
||||||
|
success,
|
||||||
|
} from '@app/event-emitter/utils/result';
|
||||||
|
|
||||||
export namespace ListIssuesByUsersLikeJiraWidgetNs {
|
export namespace ListIssuesByUsersLikeJiraWidgetNs {
|
||||||
export namespace Models {
|
export namespace Models {
|
||||||
|
|
@ -29,15 +35,17 @@ export namespace ListIssuesByUsersLikeJiraWidgetNs {
|
||||||
type Params = ListIssuesByUsersLikeJiraWidgetNs.Models.Params;
|
type Params = ListIssuesByUsersLikeJiraWidgetNs.Models.Params;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ListIssuesByUsersLikeJiraWidgetService
|
export class ListIssuesByUsersLikeJiraWidgetDataLoaderService
|
||||||
implements WidgetInterface<Params, any, any>
|
implements WidgetDataLoaderInterface<Params, any, any>
|
||||||
{
|
{
|
||||||
private logger = new Logger(ListIssuesByUsersLikeJiraWidgetService.name);
|
private logger = new Logger(
|
||||||
|
ListIssuesByUsersLikeJiraWidgetDataLoaderService.name,
|
||||||
|
);
|
||||||
private issuesLoader: IssuesServiceNs.IssuesLoader;
|
private issuesLoader: IssuesServiceNs.IssuesLoader;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private issuesService: IssuesService,
|
|
||||||
private timePassedHighlightEnhancer: TimePassedHighlightEnhancer,
|
private timePassedHighlightEnhancer: TimePassedHighlightEnhancer,
|
||||||
|
private issuesService: IssuesService,
|
||||||
private issueUrlEnhancer: IssueUrlEnhancer,
|
private issueUrlEnhancer: IssueUrlEnhancer,
|
||||||
) {
|
) {
|
||||||
this.issuesLoader = this.issuesService.createDynamicIssuesLoader();
|
this.issuesLoader = this.issuesService.createDynamicIssuesLoader();
|
||||||
|
|
@ -47,7 +55,7 @@ export class ListIssuesByUsersLikeJiraWidgetService
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(widgetParams: Params): Promise<any> {
|
async load(widgetParams: Params): Promise<Result<any, AppError>> {
|
||||||
let store: FlatIssuesStore;
|
let store: FlatIssuesStore;
|
||||||
if (widgetParams.fromRootIssueId) {
|
if (widgetParams.fromRootIssueId) {
|
||||||
store = await this.getListFromRoot(widgetParams.fromRootIssueId);
|
store = await this.getListFromRoot(widgetParams.fromRootIssueId);
|
||||||
|
|
@ -56,7 +64,7 @@ export class ListIssuesByUsersLikeJiraWidgetService
|
||||||
} else {
|
} else {
|
||||||
const errMsg = `Wrong widgetParams value`;
|
const errMsg = `Wrong widgetParams value`;
|
||||||
this.logger.error(errMsg);
|
this.logger.error(errMsg);
|
||||||
throw new Error(errMsg);
|
return fail(createAppError(errMsg));
|
||||||
}
|
}
|
||||||
await store.enhanceIssues([
|
await store.enhanceIssues([
|
||||||
this.timePassedHighlightEnhancer,
|
this.timePassedHighlightEnhancer,
|
||||||
|
|
@ -92,7 +100,7 @@ export class ListIssuesByUsersLikeJiraWidgetService
|
||||||
return a.metainfo.title.localeCompare(b.metainfo.title);
|
return a.metainfo.title.localeCompare(b.metainfo.title);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return res;
|
return success(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {
|
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {
|
||||||
|
|
@ -12,8 +12,14 @@ import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from
|
||||||
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
|
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import nano from 'nano';
|
import nano from 'nano';
|
||||||
import { WidgetInterface } from '../widget-interface';
|
|
||||||
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
|
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
|
||||||
|
import { WidgetDataLoaderInterface } from '../widget-data-loader-interface';
|
||||||
|
import {
|
||||||
|
AppError,
|
||||||
|
Result,
|
||||||
|
createAppError,
|
||||||
|
success,
|
||||||
|
} from '@app/event-emitter/utils/result';
|
||||||
|
|
||||||
export namespace ListIssuesByUsersWidgetNs {
|
export namespace ListIssuesByUsersWidgetNs {
|
||||||
export namespace Models {
|
export namespace Models {
|
||||||
|
|
@ -41,10 +47,10 @@ type ExtendedIssue = RedmineTypes.Issue & Record<string, any>;
|
||||||
type FindResult = ListIssuesByUsersWidgetNs.Models.FindResult;
|
type FindResult = ListIssuesByUsersWidgetNs.Models.FindResult;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ListIssuesByUsersWidgetService
|
export class ListIssuesByUsersWidgetDataLoaderService
|
||||||
implements WidgetInterface<Params, any, any>
|
implements WidgetDataLoaderInterface<Params, any, any>
|
||||||
{
|
{
|
||||||
private logger = new Logger(ListIssuesByUsersWidgetService.name);
|
private logger = new Logger(ListIssuesByUsersWidgetDataLoaderService.name);
|
||||||
private issuesLoader: IssuesServiceNs.IssuesLoader;
|
private issuesLoader: IssuesServiceNs.IssuesLoader;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -59,7 +65,7 @@ export class ListIssuesByUsersWidgetService
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(widgetParams: Params): Promise<any> {
|
async load(widgetParams: Params): Promise<Result<any, AppError>> {
|
||||||
let store: FlatIssuesStore;
|
let store: FlatIssuesStore;
|
||||||
if (widgetParams.fromRootIssueId) {
|
if (widgetParams.fromRootIssueId) {
|
||||||
store = await this.getListFromRoot(widgetParams.fromRootIssueId);
|
store = await this.getListFromRoot(widgetParams.fromRootIssueId);
|
||||||
|
|
@ -68,7 +74,7 @@ export class ListIssuesByUsersWidgetService
|
||||||
} else {
|
} else {
|
||||||
const errMsg = `Wrong widgetParams value`;
|
const errMsg = `Wrong widgetParams value`;
|
||||||
this.logger.error(errMsg);
|
this.logger.error(errMsg);
|
||||||
throw new Error(errMsg);
|
return fail(createAppError(errMsg));
|
||||||
}
|
}
|
||||||
await store.enhanceIssues([
|
await store.enhanceIssues([
|
||||||
this.timePassedHighlightEnhancer,
|
this.timePassedHighlightEnhancer,
|
||||||
|
|
@ -95,7 +101,7 @@ export class ListIssuesByUsersWidgetService
|
||||||
return a.metainfo.title.localeCompare(b.metainfo.title);
|
return a.metainfo.title.localeCompare(b.metainfo.title);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return res;
|
return success(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {
|
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {
|
||||||
|
|
@ -11,8 +11,9 @@ import {
|
||||||
TreeIssuesStoreNs,
|
TreeIssuesStoreNs,
|
||||||
} from '@app/event-emitter/utils/tree-issues-store';
|
} from '@app/event-emitter/utils/tree-issues-store';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { WidgetInterface } from '../widget-interface';
|
|
||||||
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
|
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
|
||||||
|
import { WidgetDataLoaderInterface } from '../widget-data-loader-interface';
|
||||||
|
import { AppError, Result, success } from '@app/event-emitter/utils/result';
|
||||||
|
|
||||||
export namespace RootIssueSubTreesWidgetNs {
|
export namespace RootIssueSubTreesWidgetNs {
|
||||||
export namespace Models {
|
export namespace Models {
|
||||||
|
|
@ -39,8 +40,8 @@ export namespace RootIssueSubTreesWidgetNs {
|
||||||
type Params = RootIssueSubTreesWidgetNs.Models.Params;
|
type Params = RootIssueSubTreesWidgetNs.Models.Params;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RootIssueSubTreesWidgetService
|
export class RootIssueSubTreesWidgetDataLoaderService
|
||||||
implements WidgetInterface<Params, any, any>
|
implements WidgetDataLoaderInterface<Params, any, any>
|
||||||
{
|
{
|
||||||
private issuesLoader: IssuesServiceNs.IssuesLoader;
|
private issuesLoader: IssuesServiceNs.IssuesLoader;
|
||||||
|
|
||||||
|
|
@ -56,7 +57,7 @@ export class RootIssueSubTreesWidgetService
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(widgetParams: Params): Promise<any> {
|
async load(widgetParams: Params): Promise<Result<any, AppError>> {
|
||||||
const treeStore = new TreeIssuesStore();
|
const treeStore = new TreeIssuesStore();
|
||||||
const rootIssue = await this.issuesService.getIssue(
|
const rootIssue = await this.issuesService.getIssue(
|
||||||
widgetParams.rootIssueId,
|
widgetParams.rootIssueId,
|
||||||
|
|
@ -89,11 +90,12 @@ export class RootIssueSubTreesWidgetService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return stories.map((s) => {
|
const res = stories.map((s) => {
|
||||||
return {
|
return {
|
||||||
data: s.store.groupByStatus(widgetParams.statuses),
|
data: s.store.groupByStatus(widgetParams.statuses),
|
||||||
metainfo: s.metainfo,
|
metainfo: s.metainfo,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
return success(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
19
libs/event-emitter/src/dashboards/widget-interface.ts
Normal file
19
libs/event-emitter/src/dashboards/widget-interface.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { AppError, Result } from '../utils/result';
|
||||||
|
import { WidgetDataLoaderInterface } from './widget-data-loader-interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - WP - widget params
|
||||||
|
* - DLP - dataloader params
|
||||||
|
* - DBP - dashboard params
|
||||||
|
* - DLR - dataloader result
|
||||||
|
* - R - result
|
||||||
|
*/
|
||||||
|
export interface WidgetInterface<WP, DLP, DBP, DLR, R> {
|
||||||
|
dataLoader: WidgetDataLoaderInterface<DLP, DBP, DLR>;
|
||||||
|
type: string;
|
||||||
|
render(
|
||||||
|
widgetParams: WP,
|
||||||
|
dataLoaderParams: DLP,
|
||||||
|
dashboardParams: DBP,
|
||||||
|
): Promise<Result<R, AppError>>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { WidgetInterface } from './widget-interface';
|
||||||
|
import { ListIssuesByFieldsWidgetDataLoaderService } from './widget-data-loader/list-issues-by-fields.widget-data-loader.service';
|
||||||
|
import { ListIssuesByUsersLikeJiraWidgetDataLoaderService } from './widget-data-loader/list-issues-by-users-like-jira.widget-data-loader.service';
|
||||||
|
import { RootIssueSubTreesWidgetDataLoaderService } from './widget-data-loader/root-issue-subtrees.widget-data-loader.service';
|
||||||
|
import { createInteractiveWidget } from './interactive-widget-factory';
|
||||||
|
import { Result, success } from '@app/event-emitter/utils/result';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WidgetsCollectionService {
|
||||||
|
collection: WidgetInterface<any, any, any, any, any>[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private listIssuesByFieldsWidgetDataLoaderService: ListIssuesByFieldsWidgetDataLoaderService,
|
||||||
|
private listIssuesByUsersLikeJiraWidgetDataLoaderService: ListIssuesByUsersLikeJiraWidgetDataLoaderService,
|
||||||
|
private rootIssueSubTreesWidgetDataLoaderService: RootIssueSubTreesWidgetDataLoaderService,
|
||||||
|
) {
|
||||||
|
const collection = [
|
||||||
|
createInteractiveWidget(
|
||||||
|
this.listIssuesByFieldsWidgetDataLoaderService,
|
||||||
|
'kanban_by_fields',
|
||||||
|
),
|
||||||
|
createInteractiveWidget(
|
||||||
|
this.listIssuesByUsersLikeJiraWidgetDataLoaderService,
|
||||||
|
'kanban_by_users',
|
||||||
|
),
|
||||||
|
createInteractiveWidget(
|
||||||
|
this.rootIssueSubTreesWidgetDataLoaderService,
|
||||||
|
'kanban_by_tree',
|
||||||
|
),
|
||||||
|
createInteractiveWidget(
|
||||||
|
this.listIssuesByFieldsWidgetDataLoaderService,
|
||||||
|
'issues_list_by_fields',
|
||||||
|
),
|
||||||
|
createInteractiveWidget(
|
||||||
|
this.listIssuesByUsersLikeJiraWidgetDataLoaderService,
|
||||||
|
'issues_list_by_users',
|
||||||
|
),
|
||||||
|
createInteractiveWidget(
|
||||||
|
this.rootIssueSubTreesWidgetDataLoaderService,
|
||||||
|
'issues_list_by_tree',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
collection.forEach((w) => this.appendWidget(w));
|
||||||
|
}
|
||||||
|
|
||||||
|
appendWidget(
|
||||||
|
widget: WidgetInterface<any, any, any, any, any>,
|
||||||
|
): Result<true, string> {
|
||||||
|
const type = widget.type;
|
||||||
|
const isExists = this.collection.find((w) => w.type === type);
|
||||||
|
if (isExists) return fail('WIDGET_WITH_SAME_TYPE_ALREADY_EXISTS');
|
||||||
|
this.collection.push(widget);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
getWidgetTypes(): string[] {
|
||||||
|
return this.collection.map((w) => w.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
getWidgetByType(
|
||||||
|
type: string,
|
||||||
|
): Result<WidgetInterface<any, any, any, any, any>, string> {
|
||||||
|
const widget = this.collection.find((w) => w.type === type);
|
||||||
|
return widget ? success(widget) : fail('WIDGET_WITH_SAME_TYPE_NOT_FOUND');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,15 +18,10 @@ import { IssuesService } from './issues/issues.service';
|
||||||
import { IssuesController } from './issues/issues.controller';
|
import { IssuesController } from './issues/issues.controller';
|
||||||
import { TimestampEnhancer } from './issue-enhancers/timestamps-enhancer';
|
import { TimestampEnhancer } from './issue-enhancers/timestamps-enhancer';
|
||||||
import { EnhancerService } from './issue-enhancers/enhancer.service';
|
import { EnhancerService } from './issue-enhancers/enhancer.service';
|
||||||
import { ProjectDashboardService } from './project-dashboard/project-dashboard.service';
|
|
||||||
import { RootIssueSubTreesWidgetService } from './project-dashboard/widgets/root-issue-subtrees.widget.service';
|
|
||||||
import { DynamicLoader } from './configs/dynamic-loader';
|
import { DynamicLoader } from './configs/dynamic-loader';
|
||||||
import { RedminePublicUrlConverter } from './converters/redmine-public-url.converter';
|
import { RedminePublicUrlConverter } from './converters/redmine-public-url.converter';
|
||||||
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 { 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 { IssuesUpdaterService } from './issues-updater/issues-updater.service';
|
import { IssuesUpdaterService } from './issues-updater/issues-updater.service';
|
||||||
import { CalendarEnhancer } from './issue-enhancers/calendar-enhancer';
|
import { CalendarEnhancer } from './issue-enhancers/calendar-enhancer';
|
||||||
import { CalendarService } from './calendar/calendar.service';
|
import { CalendarService } from './calendar/calendar.service';
|
||||||
|
|
@ -34,6 +29,12 @@ import { CalendarController } from './calendar/calendar.controller';
|
||||||
import { Dashboards as DashboardsDs } from './couchdb-datasources/dashboards';
|
import { Dashboards as DashboardsDs } from './couchdb-datasources/dashboards';
|
||||||
import { DashboardController } from './dashboards/dashboard.controller';
|
import { DashboardController } from './dashboards/dashboard.controller';
|
||||||
import { DashboardsService } from './dashboards/dashboards.service';
|
import { DashboardsService } from './dashboards/dashboards.service';
|
||||||
|
import { DashboardsDataService } from './dashboards/dashboards-data.service';
|
||||||
|
import { RootIssueSubTreesWidgetDataLoaderService } from './dashboards/widget-data-loader/root-issue-subtrees.widget-data-loader.service';
|
||||||
|
import { ListIssuesByUsersWidgetDataLoaderService } from './dashboards/widget-data-loader/list-issues-by-users.widget-data-loader.service';
|
||||||
|
import { ListIssuesByUsersLikeJiraWidgetDataLoaderService } from './dashboards/widget-data-loader/list-issues-by-users-like-jira.widget-data-loader.service';
|
||||||
|
import { ListIssuesByFieldsWidgetDataLoaderService } from './dashboards/widget-data-loader/list-issues-by-fields.widget-data-loader.service';
|
||||||
|
import { WidgetsCollectionService } from './dashboards/widgets-collection.service';
|
||||||
|
|
||||||
@Module({})
|
@Module({})
|
||||||
export class EventEmitterModule implements OnModuleInit {
|
export class EventEmitterModule implements OnModuleInit {
|
||||||
|
|
@ -57,15 +58,14 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
IssuesService,
|
IssuesService,
|
||||||
TimestampEnhancer,
|
TimestampEnhancer,
|
||||||
EnhancerService,
|
EnhancerService,
|
||||||
ProjectDashboardService,
|
RootIssueSubTreesWidgetDataLoaderService,
|
||||||
RootIssueSubTreesWidgetService,
|
|
||||||
DynamicLoader,
|
DynamicLoader,
|
||||||
RedminePublicUrlConverter,
|
RedminePublicUrlConverter,
|
||||||
IssueUrlEnhancer,
|
IssueUrlEnhancer,
|
||||||
ListIssuesByUsersWidgetService,
|
ListIssuesByUsersWidgetDataLoaderService,
|
||||||
ListIssuesByUsersLikeJiraWidgetService,
|
ListIssuesByUsersLikeJiraWidgetDataLoaderService,
|
||||||
TimePassedHighlightEnhancer,
|
TimePassedHighlightEnhancer,
|
||||||
ListIssuesByFieldsWidgetService,
|
ListIssuesByFieldsWidgetDataLoaderService,
|
||||||
{
|
{
|
||||||
provide: 'ISSUES_UPDATER_SERVICE',
|
provide: 'ISSUES_UPDATER_SERVICE',
|
||||||
useFactory: (configService: ConfigService) => {
|
useFactory: (configService: ConfigService) => {
|
||||||
|
|
@ -101,6 +101,8 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
inject: ['CALENDAR_ENHANCER', IssuesService],
|
inject: ['CALENDAR_ENHANCER', IssuesService],
|
||||||
},
|
},
|
||||||
DashboardsService,
|
DashboardsService,
|
||||||
|
DashboardsDataService,
|
||||||
|
WidgetsCollectionService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
EventEmitterService,
|
EventEmitterService,
|
||||||
|
|
@ -116,15 +118,14 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
IssuesService,
|
IssuesService,
|
||||||
TimestampEnhancer,
|
TimestampEnhancer,
|
||||||
EnhancerService,
|
EnhancerService,
|
||||||
ProjectDashboardService,
|
RootIssueSubTreesWidgetDataLoaderService,
|
||||||
RootIssueSubTreesWidgetService,
|
|
||||||
DynamicLoader,
|
DynamicLoader,
|
||||||
RedminePublicUrlConverter,
|
RedminePublicUrlConverter,
|
||||||
IssueUrlEnhancer,
|
IssueUrlEnhancer,
|
||||||
ListIssuesByUsersWidgetService,
|
ListIssuesByUsersWidgetDataLoaderService,
|
||||||
ListIssuesByUsersLikeJiraWidgetService,
|
ListIssuesByUsersLikeJiraWidgetDataLoaderService,
|
||||||
TimePassedHighlightEnhancer,
|
TimePassedHighlightEnhancer,
|
||||||
ListIssuesByFieldsWidgetService,
|
ListIssuesByFieldsWidgetDataLoaderService,
|
||||||
{
|
{
|
||||||
provide: 'ISSUES_UPDATER_SERVICE',
|
provide: 'ISSUES_UPDATER_SERVICE',
|
||||||
useExisting: 'ISSUES_UPDATER_SERVICE',
|
useExisting: 'ISSUES_UPDATER_SERVICE',
|
||||||
|
|
@ -138,6 +139,8 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
useExisting: 'CALENDAR_SERVICE',
|
useExisting: 'CALENDAR_SERVICE',
|
||||||
},
|
},
|
||||||
DashboardsService,
|
DashboardsService,
|
||||||
|
DashboardsDataService,
|
||||||
|
WidgetsCollectionService,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
MainController,
|
MainController,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,29 @@
|
||||||
export type Data = Record<string, any> | null;
|
export type Data = {
|
||||||
|
widgets: Widget[];
|
||||||
|
title: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
export type Dashboard = {
|
export type Dashboard = {
|
||||||
id: string;
|
id: string;
|
||||||
data: Data;
|
data: Data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры для отрисовки данных
|
||||||
|
*/
|
||||||
|
export type WidgetParams = {
|
||||||
|
collapsed?: boolean;
|
||||||
|
} & Record<string, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры для загрузки данных
|
||||||
|
*/
|
||||||
|
export type DataLoaderParams = Record<string, any> | null;
|
||||||
|
|
||||||
|
export type Widget = {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
widgetParams?: WidgetParams;
|
||||||
|
dataLoaderParams?: DataLoaderParams;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-namespace */
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { RedmineTypes } from '../models/redmine-types';
|
|
||||||
|
|
||||||
export namespace ProjectDashboard {
|
|
||||||
export namespace Models {
|
|
||||||
export type Params = {
|
|
||||||
projectName: string;
|
|
||||||
workers: Worker[];
|
|
||||||
filter: FilterDefination[];
|
|
||||||
statuses: Status[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Worker = {
|
|
||||||
id?: number;
|
|
||||||
firstname?: string;
|
|
||||||
lastname?: string;
|
|
||||||
name?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum FilterTypes {
|
|
||||||
TREE = 'TREE',
|
|
||||||
LIST = 'LIST',
|
|
||||||
DYNAMIC_LIST = 'DYNAMIC_LIST',
|
|
||||||
VERSION = 'VERSION',
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace FilterParams {
|
|
||||||
export type Tree = {
|
|
||||||
rootIssueId: number;
|
|
||||||
subtrees: SubTree[];
|
|
||||||
showOthers: boolean;
|
|
||||||
};
|
|
||||||
export type SubTree = {
|
|
||||||
title: string;
|
|
||||||
issueId: number;
|
|
||||||
};
|
|
||||||
export type List = {
|
|
||||||
issueIds: number[];
|
|
||||||
};
|
|
||||||
export type Version = {
|
|
||||||
version: string;
|
|
||||||
};
|
|
||||||
export type DynamicList = {
|
|
||||||
selector: Record<string, any>;
|
|
||||||
};
|
|
||||||
export type AnyFilterParams = Tree | List | DynamicList | Version;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FilterParams = Record<string, any>;
|
|
||||||
|
|
||||||
export namespace FilterResults {
|
|
||||||
export type Tree = List;
|
|
||||||
export type List = { status: string; issues: RedmineTypes.Issue[] }[];
|
|
||||||
export type DynamicList = List;
|
|
||||||
export type Version = List;
|
|
||||||
export type AnyFilterResults = List | Tree | DynamicList | Version;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FilterResult = Record<string, any>[];
|
|
||||||
|
|
||||||
export type AnalyticFunction = {
|
|
||||||
functionName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FilterDefination = {
|
|
||||||
type: FilterTypes;
|
|
||||||
title: string;
|
|
||||||
params: FilterParams.AnyFilterParams;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FilterWithResults = {
|
|
||||||
params: FilterDefination;
|
|
||||||
results: FilterResults.AnyFilterResults;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Status = {
|
|
||||||
name: string;
|
|
||||||
closed: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CheckWorker(worker: Models.Worker): boolean {
|
|
||||||
return Boolean(
|
|
||||||
(typeof worker.id === 'number' && worker.id >= 0) ||
|
|
||||||
(worker.firstname && worker.lastname) ||
|
|
||||||
worker.name,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SingleProject {
|
|
||||||
// TODO: code for SingleProject
|
|
||||||
constructor(private params: Models.Params) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace Widgets {
|
|
||||||
// Чё будет делать виджет?
|
|
||||||
// * рендер - из параметров будет создавать данные с какими-либо расчётами
|
|
||||||
|
|
||||||
export interface Widget {
|
|
||||||
render(
|
|
||||||
filterParams: Models.FilterParams,
|
|
||||||
dashboardParams: Models.Params,
|
|
||||||
): Models.FilterResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class List implements Widget {
|
|
||||||
render(
|
|
||||||
filterParams: Models.FilterParams,
|
|
||||||
dashboardParams: Models.Params,
|
|
||||||
): Models.FilterResult {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DynamicList implements Widget {
|
|
||||||
render(
|
|
||||||
filterParams: Models.FilterParams,
|
|
||||||
dashboardParams: Models.Params,
|
|
||||||
): Models.FilterResult {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Tree implements Widget {
|
|
||||||
render(
|
|
||||||
filterParams: Models.FilterParams,
|
|
||||||
dashboardParams: Models.Params,
|
|
||||||
): Models.FilterResult {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Version implements Widget {
|
|
||||||
render(
|
|
||||||
filterParams: Models.FilterParams,
|
|
||||||
dashboardParams: Models.Params,
|
|
||||||
): Models.FilterResult {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ProjectDashboardService {
|
|
||||||
constructor() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export interface WidgetInterface<W, D, R> {
|
|
||||||
isMyConfig(widgetParams: W): boolean;
|
|
||||||
render(widgetParams: W, dashboardParams: D): Promise<R>;
|
|
||||||
}
|
|
||||||
|
|
@ -43,7 +43,7 @@ import { SetDailyEccmUserCommentBotHandlerService } from './telegram-bot/handler
|
||||||
import { DailyEccmWithExtraDataService } from './reports/daily-eccm-with-extra-data.service';
|
import { DailyEccmWithExtraDataService } from './reports/daily-eccm-with-extra-data.service';
|
||||||
import { SimpleKanbanBoardController } from './dashboards/simple-kanban-board.controller';
|
import { SimpleKanbanBoardController } from './dashboards/simple-kanban-board.controller';
|
||||||
import { IssueUrlEnhancer } from '@app/event-emitter/issue-enhancers/issue-url-enhancer';
|
import { IssueUrlEnhancer } from '@app/event-emitter/issue-enhancers/issue-url-enhancer';
|
||||||
import { IssuesByTagsWidgetService } from './dashboards/widgets/issues-by-tags.widget.service';
|
import { IssuesByTagsWidgetDataLoaderService } from './dashboards/widget-data-loader/issues-by-tags.widget-data-loader.service';
|
||||||
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';
|
||||||
|
|
@ -52,6 +52,7 @@ import { TagsManagerController } from './tags-manager/tags-manager.controller';
|
||||||
import { CreateTagManagerServiceProvider } from './tags-manager/tags-manager.service';
|
import { CreateTagManagerServiceProvider } from './tags-manager/tags-manager.service';
|
||||||
import { CalendarEnhancer } from '@app/event-emitter/issue-enhancers/calendar-enhancer';
|
import { CalendarEnhancer } from '@app/event-emitter/issue-enhancers/calendar-enhancer';
|
||||||
import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasources/dashboards';
|
import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasources/dashboards';
|
||||||
|
import { DashboardInitService } from './dashboards/dashboard-init.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -99,7 +100,7 @@ import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasourc
|
||||||
DailyEccmUserCommentsService,
|
DailyEccmUserCommentsService,
|
||||||
SetDailyEccmUserCommentBotHandlerService,
|
SetDailyEccmUserCommentBotHandlerService,
|
||||||
DailyEccmWithExtraDataService,
|
DailyEccmWithExtraDataService,
|
||||||
IssuesByTagsWidgetService,
|
IssuesByTagsWidgetDataLoaderService,
|
||||||
{
|
{
|
||||||
provide: 'CATEGORY_MERGE_TO_TAGS_ENHANCER',
|
provide: 'CATEGORY_MERGE_TO_TAGS_ENHANCER',
|
||||||
useFactory: (configService: ConfigService) => {
|
useFactory: (configService: ConfigService) => {
|
||||||
|
|
@ -111,6 +112,7 @@ import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasourc
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
},
|
},
|
||||||
CreateTagManagerServiceProvider('TAG_MANAGER_SERVICE'),
|
CreateTagManagerServiceProvider('TAG_MANAGER_SERVICE'),
|
||||||
|
DashboardInitService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements OnModuleInit {
|
export class AppModule implements OnModuleInit {
|
||||||
|
|
@ -137,6 +139,8 @@ export class AppModule implements OnModuleInit {
|
||||||
|
|
||||||
@Inject('CALENDAR_ENHANCER')
|
@Inject('CALENDAR_ENHANCER')
|
||||||
private calendarEnhancer: CalendarEnhancer,
|
private calendarEnhancer: CalendarEnhancer,
|
||||||
|
|
||||||
|
private dashboardInitService: DashboardInitService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
|
|
@ -215,6 +219,7 @@ export class AppModule implements OnModuleInit {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.initDailyEccmUserCommentsPipeline();
|
this.initDailyEccmUserCommentsPipeline();
|
||||||
|
this.initDashbordProviders();
|
||||||
}
|
}
|
||||||
|
|
||||||
private initDailyEccmUserCommentsPipeline(): void {
|
private initDailyEccmUserCommentsPipeline(): void {
|
||||||
|
|
@ -228,4 +233,8 @@ export class AppModule implements OnModuleInit {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initDashbordProviders(): void {
|
||||||
|
this.dashboardInitService.init();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
src/dashboards/dashboard-init.service.ts
Normal file
26
src/dashboards/dashboard-init.service.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { WidgetsCollectionService } from '@app/event-emitter/dashboards/widgets-collection.service';
|
||||||
|
import { IssuesByTagsWidgetDataLoaderService } from './widget-data-loader/issues-by-tags.widget-data-loader.service';
|
||||||
|
import { createInteractiveWidget } from '@app/event-emitter/dashboards/interactive-widget-factory';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DashboardInitService {
|
||||||
|
constructor(
|
||||||
|
private issuesByTagsWidgetDataLoaderService: IssuesByTagsWidgetDataLoaderService,
|
||||||
|
private widgetsCollectionService: WidgetsCollectionService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
const collection = [
|
||||||
|
createInteractiveWidget(
|
||||||
|
this.issuesByTagsWidgetDataLoaderService,
|
||||||
|
'kanban_by_tags',
|
||||||
|
),
|
||||||
|
createInteractiveWidget(
|
||||||
|
this.issuesByTagsWidgetDataLoaderService,
|
||||||
|
'issues_list_by_tags',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
collection.forEach((w) => this.widgetsCollectionService.appendWidget(w));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader';
|
import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader';
|
||||||
import { Controller, Get, Param, Render } from '@nestjs/common';
|
import { Controller, Get, Param, Render } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { IssuesByTagsWidgetService } from './widgets/issues-by-tags.widget.service';
|
import { IssuesByTagsWidgetDataLoaderService } from './widget-data-loader/issues-by-tags.widget-data-loader.service';
|
||||||
import { parse } from 'jsonc-parser';
|
import { parse } from 'jsonc-parser';
|
||||||
|
|
||||||
@Controller('simple-issues-list')
|
@Controller('simple-issues-list')
|
||||||
|
|
@ -9,7 +9,7 @@ export class SimpleIssuesListController {
|
||||||
private path: string;
|
private path: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private issuesByTagsWidgetService: IssuesByTagsWidgetService,
|
private issuesByTagsWidgetDataLoaderService: IssuesByTagsWidgetDataLoaderService,
|
||||||
private dynamicLoader: DynamicLoader,
|
private dynamicLoader: DynamicLoader,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
|
|
@ -23,7 +23,7 @@ export class SimpleIssuesListController {
|
||||||
ext: 'jsonc',
|
ext: 'jsonc',
|
||||||
parser: parse,
|
parser: parse,
|
||||||
});
|
});
|
||||||
return await this.issuesByTagsWidgetService.render(cfg);
|
return await this.issuesByTagsWidgetDataLoaderService.load(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/by-tags/:name')
|
@Get('/by-tags/:name')
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,14 @@
|
||||||
import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader';
|
import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader';
|
||||||
import { RedmineEventsGateway } from '@app/event-emitter/events/redmine-events.gateway';
|
import { RedmineEventsGateway } from '@app/event-emitter/events/redmine-events.gateway';
|
||||||
import { IssuesService } from '@app/event-emitter/issues/issues.service';
|
import { IssuesService } from '@app/event-emitter/issues/issues.service';
|
||||||
import { ListIssuesByFieldsWidgetService } from '@app/event-emitter/project-dashboard/widgets/list-issues-by-fields.widget.service';
|
|
||||||
import { ListIssuesByUsersLikeJiraWidgetService } from '@app/event-emitter/project-dashboard/widgets/list-issues-by-users-like-jira.widget.service';
|
|
||||||
import { ListIssuesByUsersWidgetService } from '@app/event-emitter/project-dashboard/widgets/list-issues-by-users.widget.service';
|
|
||||||
import {
|
|
||||||
RootIssueSubTreesWidgetNs,
|
|
||||||
RootIssueSubTreesWidgetService,
|
|
||||||
} from '@app/event-emitter/project-dashboard/widgets/root-issue-subtrees.widget.service';
|
|
||||||
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
|
|
||||||
import { Controller, Get, Logger, Param, Render } from '@nestjs/common';
|
import { Controller, Get, Logger, Param, Render } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { parse } from 'jsonc-parser';
|
import { parse } from 'jsonc-parser';
|
||||||
import { IssuesByTagsWidgetService } from './widgets/issues-by-tags.widget.service';
|
import { IssuesByTagsWidgetDataLoaderService } from './widget-data-loader/issues-by-tags.widget-data-loader.service';
|
||||||
|
import { RootIssueSubTreesWidgetDataLoaderService } from '@app/event-emitter/dashboards/widget-data-loader/root-issue-subtrees.widget-data-loader.service';
|
||||||
|
import { ListIssuesByUsersWidgetDataLoaderService } from '@app/event-emitter/dashboards/widget-data-loader/list-issues-by-users.widget-data-loader.service';
|
||||||
|
import { ListIssuesByUsersLikeJiraWidgetDataLoaderService } from '@app/event-emitter/dashboards/widget-data-loader/list-issues-by-users-like-jira.widget-data-loader.service';
|
||||||
|
import { ListIssuesByFieldsWidgetDataLoaderService } from '@app/event-emitter/dashboards/widget-data-loader/list-issues-by-fields.widget-data-loader.service';
|
||||||
|
|
||||||
@Controller('simple-kanban-board')
|
@Controller('simple-kanban-board')
|
||||||
export class SimpleKanbanBoardController {
|
export class SimpleKanbanBoardController {
|
||||||
|
|
@ -20,14 +16,14 @@ export class SimpleKanbanBoardController {
|
||||||
private path: string;
|
private path: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private rootIssueSubTreesWidgetService: RootIssueSubTreesWidgetService,
|
private rootIssueSubTreesWidgetDataLoaderService: RootIssueSubTreesWidgetDataLoaderService,
|
||||||
private dynamicLoader: DynamicLoader,
|
private dynamicLoader: DynamicLoader,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private listIssuesByUsersWidgetService: ListIssuesByUsersWidgetService,
|
private listIssuesByUsersWidgetDataLoaderService: ListIssuesByUsersWidgetDataLoaderService,
|
||||||
private listIssuesByUsersLikeJiraWidgetService: ListIssuesByUsersLikeJiraWidgetService,
|
private listIssuesByUsersLikeJiraWidgetDataLoaderService: ListIssuesByUsersLikeJiraWidgetDataLoaderService,
|
||||||
private issuesByTagsWidgetService: IssuesByTagsWidgetService,
|
private issuesByTagsWidgetDataLoaderService: IssuesByTagsWidgetDataLoaderService,
|
||||||
private redmineEventsGateway: RedmineEventsGateway,
|
private redmineEventsGateway: RedmineEventsGateway,
|
||||||
private listIssuesByFieldsWidgetService: ListIssuesByFieldsWidgetService,
|
private listIssuesByFieldsWidgetDataLoaderService: ListIssuesByFieldsWidgetDataLoaderService,
|
||||||
private issuesService: IssuesService,
|
private issuesService: IssuesService,
|
||||||
) {
|
) {
|
||||||
this.path = this.configService.get<string>('simpleKanbanBoard.path');
|
this.path = this.configService.get<string>('simpleKanbanBoard.path');
|
||||||
|
|
@ -40,7 +36,7 @@ export class SimpleKanbanBoardController {
|
||||||
ext: 'jsonc',
|
ext: 'jsonc',
|
||||||
parser: parse,
|
parser: parse,
|
||||||
});
|
});
|
||||||
return await this.rootIssueSubTreesWidgetService.render(cfg);
|
return await this.rootIssueSubTreesWidgetDataLoaderService.load(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/tree/:name')
|
@Get('/tree/:name')
|
||||||
|
|
@ -73,7 +69,7 @@ export class SimpleKanbanBoardController {
|
||||||
ext: 'jsonc',
|
ext: 'jsonc',
|
||||||
parser: parse,
|
parser: parse,
|
||||||
});
|
});
|
||||||
return await this.listIssuesByUsersWidgetService.render(cfg);
|
return await this.listIssuesByUsersWidgetDataLoaderService.load(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/by-users/:name')
|
@Get('/by-users/:name')
|
||||||
|
|
@ -89,7 +85,9 @@ export class SimpleKanbanBoardController {
|
||||||
ext: 'jsonc',
|
ext: 'jsonc',
|
||||||
parser: parse,
|
parser: parse,
|
||||||
});
|
});
|
||||||
return await this.listIssuesByUsersLikeJiraWidgetService.render(cfg);
|
return await this.listIssuesByUsersLikeJiraWidgetDataLoaderService.load(
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/by-users-like-jira/:name')
|
@Get('/by-users-like-jira/:name')
|
||||||
|
|
@ -105,7 +103,7 @@ export class SimpleKanbanBoardController {
|
||||||
ext: 'jsonc',
|
ext: 'jsonc',
|
||||||
parser: parse,
|
parser: parse,
|
||||||
});
|
});
|
||||||
return await this.issuesByTagsWidgetService.render(cfg);
|
return await this.issuesByTagsWidgetDataLoaderService.load(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/by-tags/:name')
|
@Get('/by-tags/:name')
|
||||||
|
|
@ -121,7 +119,7 @@ export class SimpleKanbanBoardController {
|
||||||
ext: 'jsonc',
|
ext: 'jsonc',
|
||||||
parser: parse,
|
parser: parse,
|
||||||
});
|
});
|
||||||
return await this.listIssuesByFieldsWidgetService.render(cfg);
|
return await this.listIssuesByFieldsWidgetDataLoaderService.load(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/by-fields/:name')
|
@Get('/by-fields/:name')
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,19 @@ import {
|
||||||
IssuesService,
|
IssuesService,
|
||||||
IssuesServiceNs,
|
IssuesServiceNs,
|
||||||
} from '@app/event-emitter/issues/issues.service';
|
} from '@app/event-emitter/issues/issues.service';
|
||||||
import { WidgetInterface } from '@app/event-emitter/project-dashboard/widget-interface';
|
|
||||||
import { FlatIssuesStore } from '@app/event-emitter/utils/flat-issues-store';
|
import { FlatIssuesStore } from '@app/event-emitter/utils/flat-issues-store';
|
||||||
import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from-object-by-key';
|
import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from-object-by-key';
|
||||||
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
|
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import nano from 'nano';
|
import nano from 'nano';
|
||||||
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
|
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
|
||||||
|
import { WidgetDataLoaderInterface } from '@app/event-emitter/dashboards/widget-data-loader-interface';
|
||||||
|
import {
|
||||||
|
AppError,
|
||||||
|
Result,
|
||||||
|
createAppError,
|
||||||
|
success,
|
||||||
|
} from '@app/event-emitter/utils/result';
|
||||||
|
|
||||||
export namespace IssuesByTagsWidgetNs {
|
export namespace IssuesByTagsWidgetNs {
|
||||||
export type Params = {
|
export type Params = {
|
||||||
|
|
@ -27,10 +33,10 @@ export namespace IssuesByTagsWidgetNs {
|
||||||
type Params = IssuesByTagsWidgetNs.Params;
|
type Params = IssuesByTagsWidgetNs.Params;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class IssuesByTagsWidgetService
|
export class IssuesByTagsWidgetDataLoaderService
|
||||||
implements WidgetInterface<Params, any, any>
|
implements WidgetDataLoaderInterface<Params, any, any>
|
||||||
{
|
{
|
||||||
private logger = new Logger(IssuesByTagsWidgetService.name);
|
private logger = new Logger(IssuesByTagsWidgetDataLoaderService.name);
|
||||||
private issuesLoader: IssuesServiceNs.IssuesLoader;
|
private issuesLoader: IssuesServiceNs.IssuesLoader;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -45,7 +51,7 @@ export class IssuesByTagsWidgetService
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(widgetParams: Params): Promise<any> {
|
async load(widgetParams: Params): Promise<Result<any, AppError>> {
|
||||||
let store: FlatIssuesStore;
|
let store: FlatIssuesStore;
|
||||||
if (widgetParams.fromRootIssueId) {
|
if (widgetParams.fromRootIssueId) {
|
||||||
store = await this.getListFromRoot(widgetParams.fromRootIssueId);
|
store = await this.getListFromRoot(widgetParams.fromRootIssueId);
|
||||||
|
|
@ -54,7 +60,7 @@ export class IssuesByTagsWidgetService
|
||||||
} else {
|
} else {
|
||||||
const errMsg = `Wrong widgetParams value`;
|
const errMsg = `Wrong widgetParams value`;
|
||||||
this.logger.error(errMsg);
|
this.logger.error(errMsg);
|
||||||
throw new Error(errMsg);
|
return fail(createAppError(errMsg));
|
||||||
}
|
}
|
||||||
await store.enhanceIssues([
|
await store.enhanceIssues([
|
||||||
this.timePassedHighlightEnhancer,
|
this.timePassedHighlightEnhancer,
|
||||||
|
|
@ -100,7 +106,7 @@ export class IssuesByTagsWidgetService
|
||||||
return a.metainfo.title.localeCompare(b.metainfo.title);
|
return a.metainfo.title.localeCompare(b.metainfo.title);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return res;
|
return success(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {
|
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {
|
||||||
Loading…
Reference in a new issue