Добавлена новая доска по работникам как старая, только как в jira

This commit is contained in:
Pavel Gnedov 2023-02-13 08:10:51 +07:00
parent eacbad3a02
commit 1f3964f5ca
8 changed files with 230 additions and 44 deletions

View file

@ -24,6 +24,7 @@ import { DynamicLoader } from './configs/dynamic-loader';
import { RedminePublicUrlConverter } from './converters/redmine-public-url.converter';
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';
@Module({})
export class EventEmitterModule implements OnModuleInit {
@ -52,6 +53,7 @@ export class EventEmitterModule implements OnModuleInit {
RedminePublicUrlConverter,
IssueUrlEnhancer,
ListIssuesByUsersWidgetService,
ListIssuesByUsersLikeJiraWidgetService,
],
exports: [
EventEmitterService,
@ -72,6 +74,7 @@ export class EventEmitterModule implements OnModuleInit {
RedminePublicUrlConverter,
IssueUrlEnhancer,
ListIssuesByUsersWidgetService,
ListIssuesByUsersLikeJiraWidgetService,
],
controllers: [MainController, UsersController, IssuesController],
};

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { RedmineTypes } from '../models/redmine-types';
import { CacheTTL, Injectable, Logger } from '@nestjs/common';
import { Issues } from '../couchdb-datasources/issues';
@ -12,6 +13,12 @@ import { GetParentsHint } from '../utils/get-parents-hint';
export const ISSUE_MEMORY_CACHE_LIFETIME = 30 * 1000;
const ISSUE_MEMORY_CACHE_AUTOCLEAN_INTERVAL = 1000 * 60 * 5;
export namespace IssuesServiceNs {
export type IssuesLoader = (
ids: number[],
) => Promise<Record<number, RedmineTypes.Issue | null>>;
}
@Injectable()
export class IssuesService {
private logger = new Logger(IssuesService.name);
@ -143,4 +150,19 @@ export class IssuesService {
return null;
}
}
createDynamicIssuesLoader(): IssuesServiceNs.IssuesLoader {
const fn = async (
ids: number[],
): Promise<Record<number, RedmineTypes.Issue>> => {
const issues = await this.getIssues(ids);
const res = {} as Record<number, RedmineTypes.Issue | null>;
for (let i = 0; i < issues.length; i++) {
const issue = issues[i];
res[issue.id] = issue;
}
return res;
};
return fn;
}
}

View file

@ -0,0 +1,109 @@
/* eslint-disable @typescript-eslint/no-namespace */
import {
IssuesService,
IssuesServiceNs,
} from '@app/event-emitter/issues/issues.service';
import { FlatIssuesStore } from '@app/event-emitter/utils/flat-issues-store';
import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from-object-by-key';
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
import { Injectable, Logger } from '@nestjs/common';
import nano from 'nano';
import { WidgetInterface } from '../widget-interface';
export namespace ListIssuesByUsersLikeJiraWidgetNs {
export namespace Models {
export type Params = {
fromRootIssueId?: number;
fromQuery?: nano.MangoQuery;
userKeys: string[];
userSort?: boolean;
statuses: string[];
};
}
}
type Params = ListIssuesByUsersLikeJiraWidgetNs.Models.Params;
@Injectable()
export class ListIssuesByUsersLikeJiraWidgetService
implements WidgetInterface<Params, any, any>
{
private logger = new Logger(ListIssuesByUsersLikeJiraWidgetService.name);
private issuesLoader: IssuesServiceNs.IssuesLoader;
constructor(private issuesService: IssuesService) {
this.issuesLoader = this.issuesService.createDynamicIssuesLoader();
}
isMyConfig(): boolean {
return true;
}
async render(widgetParams: Params): Promise<any> {
let store: FlatIssuesStore;
if (widgetParams.fromRootIssueId) {
store = await this.getListFromRoot(widgetParams.fromRootIssueId);
} else if (widgetParams.fromQuery) {
store = await this.getListByQuery(widgetParams.fromQuery);
} else {
const errMsg = `Wrong widgetParams value`;
this.logger.error(errMsg);
throw new Error(errMsg);
}
const grouped = store.groupByStatusWithExtraToMultipleStories((issue) => {
const users = [] as string[];
for (let i = 0; i < widgetParams.userKeys.length; i++) {
const userKey = widgetParams.userKeys[i];
const userValue = GetValueFromObjectByKey(issue, userKey);
if (userValue.result) {
users.push(userValue.result);
} else {
users.push('Unknown Unknown');
}
}
return users;
}, widgetParams.statuses);
let res = [] as any[];
for (const user in grouped) {
if (Object.prototype.hasOwnProperty.call(grouped, user)) {
const data = grouped[user];
res.push({
data: data,
metainfo: this.createMetaInfo(user),
});
}
}
if (widgetParams.userSort) {
res = res.sort((a, b) => {
return a.metainfo.title.localeCompare(b.metainfo.title);
});
}
return res;
}
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {
const treeStore = new TreeIssuesStore();
const rootIssue = await this.issuesService.getIssue(issueId);
treeStore.setRootIssue(rootIssue);
await treeStore.fillData(this.issuesLoader);
return treeStore.getFlatStore();
}
private async getListByQuery(
query: nano.MangoQuery,
): Promise<FlatIssuesStore> {
const rawData = await this.issuesService.find(query);
const store = new FlatIssuesStore();
for (let i = 0; i < rawData.length; i++) {
const issue = rawData[i];
store.push(issue);
}
return store;
}
private createMetaInfo(user: string): Record<string, any> {
return {
title: user,
};
}
}

View file

@ -1,7 +1,11 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { IssuesService } from '@app/event-emitter/issues/issues.service';
import {
IssuesService,
IssuesServiceNs,
} from '@app/event-emitter/issues/issues.service';
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
import { FlatIssuesStore } from '@app/event-emitter/utils/flat-issues-store';
import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from-object-by-key';
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
import { Injectable, Logger } from '@nestjs/common';
import nano from 'nano';
@ -37,9 +41,10 @@ export class ListIssuesByUsersWidgetService
implements WidgetInterface<Params, any, any>
{
private logger = new Logger(ListIssuesByUsersWidgetService.name);
private issuesLoader: IssuesServiceNs.IssuesLoader;
constructor(private issuesService: IssuesService) {
return;
this.issuesLoader = this.issuesService.createDynamicIssuesLoader();
}
isMyConfig(): boolean {
@ -54,7 +59,7 @@ export class ListIssuesByUsersWidgetService
store = await this.getListByQuery(widgetParams.fromQuery);
} else {
const errMsg = `Wrong widgetParams value`;
this.logger.error(`Wrong widgetParams value`);
this.logger.error(errMsg);
throw new Error(errMsg);
}
const grouped = store.groupByStatusWithExtra((issue) => {
@ -83,7 +88,7 @@ export class ListIssuesByUsersWidgetService
const treeStore = new TreeIssuesStore();
const rootIssue = await this.issuesService.getIssue(issueId);
treeStore.setRootIssue(rootIssue);
await treeStore.fillData(this.issuesLoader.bind(this));
await treeStore.fillData(this.issuesLoader);
return treeStore.getFlatStore();
}
@ -99,29 +104,13 @@ export class ListIssuesByUsersWidgetService
return store;
}
private async issuesLoader(
ids: number[],
): Promise<Record<number, RedmineTypes.Issue | null>> {
const issues = await this.issuesService.getIssues(ids);
const res = {} as Record<number, RedmineTypes.Issue | null>;
for (let i = 0; i < issues.length; i++) {
const issue = issues[i];
res[issue.id] = issue;
}
return res;
}
private getUserValueByKey(issue: ExtendedIssue, key: string): FindResult {
const keys = key.split('.');
let res: any = issue;
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
if (!res.hasOwnProperty(k)) {
return { error: ListIssuesByUsersWidgetNs.Models.FindErrors.NOT_FOUND };
}
res = res[k];
const value = GetValueFromObjectByKey(issue, key);
if (value.result) {
return { result: value.result };
} else {
return { error: ListIssuesByUsersWidgetNs.Models.FindErrors.NOT_FOUND };
}
return { result: res };
}
private createMetaInfo(user: string): Record<string, any> {

View file

@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { IssuesService } from '@app/event-emitter/issues/issues.service';
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
import {
IssuesService,
IssuesServiceNs,
} from '@app/event-emitter/issues/issues.service';
import {
TreeIssuesStore,
TreeIssuesStoreNs,
@ -36,7 +38,11 @@ type Params = RootIssueSubTreesWidgetNs.Models.Params;
export class RootIssueSubTreesWidgetService
implements WidgetInterface<Params, any, any>
{
constructor(private issuesService: IssuesService) {}
private issuesLoader: IssuesServiceNs.IssuesLoader;
constructor(private issuesService: IssuesService) {
this.issuesLoader = this.issuesService.createDynamicIssuesLoader();
}
isMyConfig(): boolean {
return true;
@ -48,7 +54,7 @@ export class RootIssueSubTreesWidgetService
widgetParams.rootIssueId,
);
treeStore.setRootIssue(rootIssue);
await treeStore.fillData(this.issuesLoader.bind(this));
await treeStore.fillData(this.issuesLoader);
let stories: TreeIssuesStoreNs.Models.GetFlatStories.Result;
if (widgetParams.parentsAsGroups) {
stories = treeStore.getFlatStoriesByParents();
@ -76,16 +82,4 @@ export class RootIssueSubTreesWidgetService
};
});
}
private async issuesLoader(
ids: number[],
): Promise<Record<number, RedmineTypes.Issue | null>> {
const issues = await this.issuesService.getIssues(ids);
const res = {} as Record<number, RedmineTypes.Issue | null>;
for (let i = 0; i < issues.length; i++) {
const issue = issues[i];
res[issue.id] = issue;
}
return res;
}
}

View file

@ -1,10 +1,9 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { IssuesServiceNs } from '../issues/issues.service';
import { RedmineTypes } from '../models/redmine-types';
export namespace FlatIssuesStoreNs {
export type IssuesLoader = (
ids: number[],
) => Promise<Record<number, RedmineTypes.Issue | null>>;
export type IssuesLoader = IssuesServiceNs.IssuesLoader;
export namespace Models {
export type ByStatus = {
@ -178,4 +177,41 @@ export class FlatIssuesStore {
}
return res;
}
groupByToMultipleStories(
iteratee: (issue: RedmineTypes.Issue) => (string | number)[],
): Record<string | number, FlatIssuesStore> {
const res = {} as Record<string | number, FlatIssuesStore>;
const items = this.getIssues();
for (let i = 0; i < items.length; i++) {
const issue = items[i];
const keys = iteratee(issue);
for (let j = 0; j < keys.length; j++) {
const key = keys[j];
if (!Object.prototype.hasOwnProperty.call(res, key)) {
res[key] = new FlatIssuesStore();
}
res[key].push(issue);
}
}
return res;
}
groupByStatusWithExtraToMultipleStories(
iteratee: (issue: RedmineTypes.Issue) => (string | number)[],
statuses: string[],
): Record<string | number, FlatIssuesStoreNs.Models.ByStatuses> {
const res = {} as Record<
string | number,
FlatIssuesStoreNs.Models.ByStatuses
>;
const groupedIssues = this.groupByToMultipleStories(iteratee);
for (const key in groupedIssues) {
if (Object.prototype.hasOwnProperty.call(groupedIssues, key)) {
const store = groupedIssues[key];
res[key] = store.groupByStatus(statuses);
}
}
return res;
}
}

View file

@ -0,0 +1,15 @@
export function GetValueFromObjectByKey(
obj: any,
key: string,
): { result?: any; error?: string } {
const keys = key.split('.');
let res: any = obj;
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
if (!res.hasOwnProperty(k)) {
return { error: 'NOT_FOUND' };
}
res = res[k];
}
return { result: res };
}

View file

@ -1,4 +1,5 @@
import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader';
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 { RootIssueSubTreesWidgetService } from '@app/event-emitter/project-dashboard/widgets/root-issue-subtrees.widget.service';
import { Controller, Get, Param, Render } from '@nestjs/common';
@ -14,6 +15,7 @@ export class SimpleKanbanBoardController {
private dynamicLoader: DynamicLoader,
private configService: ConfigService,
private listIssuesByUsersWidgetService: ListIssuesByUsersWidgetService,
private listIssuesByUsersLikeJiraWidgetService: ListIssuesByUsersLikeJiraWidgetService,
) {
this.path = this.configService.get<string>('simpleKanbanBoard.path');
}
@ -49,4 +51,20 @@ export class SimpleKanbanBoardController {
async getByUsers(@Param('name') name: string): Promise<any> {
return await this.getByUsersRawData(name);
}
@Get('/by-users-like-jira/:name/raw')
async getByUsersLikeJiraRawData(@Param('name') name: string): Promise<any> {
const cfg = this.dynamicLoader.load(name, {
path: this.path,
ext: 'jsonc',
parser: parse,
});
return await this.listIssuesByUsersLikeJiraWidgetService.render(cfg);
}
@Get('/by-users-like-jira/:name')
@Render('simple-kanban-board')
async getByUsersLikeJira(@Param('name') name: string): Promise<any> {
return await this.getByUsersLikeJiraRawData(name);
}
}