Добавлена динамическая загрузка досок kanban
This commit is contained in:
parent
c3ad1df1b6
commit
99b31164cf
13 changed files with 201 additions and 42 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -41,3 +41,5 @@ configs/redmine-status-changes-config.jsonc
|
||||||
configs/eccm-versions-config.jsonc
|
configs/eccm-versions-config.jsonc
|
||||||
configs/eccm-config.jsonc
|
configs/eccm-config.jsonc
|
||||||
configs/current-user-rules.jsonc
|
configs/current-user-rules.jsonc
|
||||||
|
configs/simple-kanban-board-config.jsonc
|
||||||
|
configs/kanban-boards/
|
||||||
|
|
|
||||||
3
configs/simple-kanban-board-config.jsonc.dist
Normal file
3
configs/simple-kanban-board-config.jsonc.dist
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"path": ""
|
||||||
|
}
|
||||||
27
libs/event-emitter/src/configs/dynamic-loader.ts
Normal file
27
libs/event-emitter/src/configs/dynamic-loader.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { CacheTTL, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { readFileSync, existsSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DynamicLoader {
|
||||||
|
private logger = new Logger(DynamicLoader.name);
|
||||||
|
|
||||||
|
@CacheTTL(60)
|
||||||
|
load(
|
||||||
|
file: string,
|
||||||
|
opts: { path: string; ext: string; parser: (raw: any) => any },
|
||||||
|
): any {
|
||||||
|
const fullFileName = join(opts.path, `${file}.${opts.ext}`);
|
||||||
|
if (!existsSync(fullFileName)) return null;
|
||||||
|
let rawData: any;
|
||||||
|
let data: any;
|
||||||
|
try {
|
||||||
|
rawData = readFileSync(fullFileName, { encoding: 'utf-8' });
|
||||||
|
data = opts.parser(rawData);
|
||||||
|
} catch (ex) {
|
||||||
|
this.logger.error(`Error at config read - ${ex}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ 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 { ProjectDashboardService } from './project-dashboard/project-dashboard.service';
|
||||||
import { RootIssueSubTreesWidgetService } from './project-dashboard/widgets/root-issue-subtrees.widget.service';
|
import { RootIssueSubTreesWidgetService } from './project-dashboard/widgets/root-issue-subtrees.widget.service';
|
||||||
|
import { DynamicLoader } from './configs/dynamic-loader';
|
||||||
|
|
||||||
@Module({})
|
@Module({})
|
||||||
export class EventEmitterModule implements OnModuleInit {
|
export class EventEmitterModule implements OnModuleInit {
|
||||||
|
|
@ -44,6 +45,7 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
EnhancerService,
|
EnhancerService,
|
||||||
ProjectDashboardService,
|
ProjectDashboardService,
|
||||||
RootIssueSubTreesWidgetService,
|
RootIssueSubTreesWidgetService,
|
||||||
|
DynamicLoader,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
EventEmitterService,
|
EventEmitterService,
|
||||||
|
|
@ -60,6 +62,7 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
EnhancerService,
|
EnhancerService,
|
||||||
ProjectDashboardService,
|
ProjectDashboardService,
|
||||||
RootIssueSubTreesWidgetService,
|
RootIssueSubTreesWidgetService,
|
||||||
|
DynamicLoader,
|
||||||
],
|
],
|
||||||
controllers: [MainController, UsersController, IssuesController],
|
controllers: [MainController, UsersController, IssuesController],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
39
package-lock.json
generated
39
package-lock.json
generated
|
|
@ -22,6 +22,7 @@
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"hbs": "^4.2.0",
|
"hbs": "^4.2.0",
|
||||||
"imap-simple": "^5.1.0",
|
"imap-simple": "^5.1.0",
|
||||||
|
"jsonc-parser": "^3.2.0",
|
||||||
"luxon": "^3.1.0",
|
"luxon": "^3.1.0",
|
||||||
"nano": "^10.0.0",
|
"nano": "^10.0.0",
|
||||||
"node-telegram-bot-api": "^0.59.0",
|
"node-telegram-bot-api": "^0.59.0",
|
||||||
|
|
@ -198,6 +199,12 @@
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular-devkit/schematics/node_modules/jsonc-parser": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@angular-devkit/schematics/node_modules/rxjs": {
|
"node_modules/@angular-devkit/schematics/node_modules/rxjs": {
|
||||||
"version": "6.6.7",
|
"version": "6.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
||||||
|
|
@ -1707,6 +1714,12 @@
|
||||||
"yarn": ">= 1.13.0"
|
"yarn": ">= 1.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/schematics/node_modules/jsonc-parser": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@nestjs/schematics/node_modules/rxjs": {
|
"node_modules/@nestjs/schematics/node_modules/rxjs": {
|
||||||
"version": "6.6.7",
|
"version": "6.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
||||||
|
|
@ -7091,10 +7104,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsonc-parser": {
|
"node_modules/jsonc-parser": {
|
||||||
"version": "3.0.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||||
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==",
|
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/jsonfile": {
|
"node_modules/jsonfile": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
|
|
@ -10448,6 +10460,12 @@
|
||||||
"rxjs": "6.6.7"
|
"rxjs": "6.6.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jsonc-parser": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"rxjs": {
|
"rxjs": {
|
||||||
"version": "6.6.7",
|
"version": "6.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
||||||
|
|
@ -11625,6 +11643,12 @@
|
||||||
"rxjs": "6.6.7"
|
"rxjs": "6.6.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jsonc-parser": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"rxjs": {
|
"rxjs": {
|
||||||
"version": "6.6.7",
|
"version": "6.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
||||||
|
|
@ -15740,10 +15764,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"jsonc-parser": {
|
"jsonc-parser": {
|
||||||
"version": "3.0.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||||
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==",
|
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"jsonfile": {
|
"jsonfile": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"hbs": "^4.2.0",
|
"hbs": "^4.2.0",
|
||||||
"imap-simple": "^5.1.0",
|
"imap-simple": "^5.1.0",
|
||||||
|
"jsonc-parser": "^3.2.0",
|
||||||
"luxon": "^3.1.0",
|
"luxon": "^3.1.0",
|
||||||
"nano": "^10.0.0",
|
"nano": "^10.0.0",
|
||||||
"node-telegram-bot-api": "^0.59.0",
|
"node-telegram-bot-api": "^0.59.0",
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ import { DailyEccmReportsUserCommentsDatasource } from './couchdb-datasources/da
|
||||||
import { DailyEccmUserCommentsService } from './reports/daily-eccm-user-comments.service';
|
import { DailyEccmUserCommentsService } from './reports/daily-eccm-user-comments.service';
|
||||||
import { SetDailyEccmUserCommentBotHandlerService } from './telegram-bot/handlers/set-daily-eccm-user-comment.bot-handler.service';
|
import { SetDailyEccmUserCommentBotHandlerService } from './telegram-bot/handlers/set-daily-eccm-user-comment.bot-handler.service';
|
||||||
import { DailyEccmWithExtraDataService } from './reports/daily-eccm-with-extra-data.service';
|
import { DailyEccmWithExtraDataService } from './reports/daily-eccm-with-extra-data.service';
|
||||||
import { Eccm110DashboardController } from './dashboards/eccm110-dashboard.controller';
|
import { SimpleKanbanBoardController } from './dashboards/simple-kanban-board.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -54,7 +54,7 @@ import { Eccm110DashboardController } from './dashboards/eccm110-dashboard.contr
|
||||||
MainController,
|
MainController,
|
||||||
CurrentIssuesEccmReportController,
|
CurrentIssuesEccmReportController,
|
||||||
DailyEccmReportController,
|
DailyEccmReportController,
|
||||||
Eccm110DashboardController,
|
SimpleKanbanBoardController,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AppService,
|
AppService,
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@ import RedmineStatusesConfigLoader from './statuses.config';
|
||||||
import RedmineStatusChangesConfigLoader from './status-changes.config';
|
import RedmineStatusChangesConfigLoader from './status-changes.config';
|
||||||
import RedmineEccmConfig from './eccm.config';
|
import RedmineEccmConfig from './eccm.config';
|
||||||
import RedmineCurrentUserRulesConfig from './current-user-rules.config';
|
import RedmineCurrentUserRulesConfig from './current-user-rules.config';
|
||||||
|
import SimpleKanbanBoardConfig from './simple-kanban-board.config';
|
||||||
|
|
||||||
const redmineIssueEventEmitterConfig = RedmineIssueEventEmitterConfigLoader();
|
const redmineIssueEventEmitterConfig = RedmineIssueEventEmitterConfigLoader();
|
||||||
const redmineStatusesConfig = RedmineStatusesConfigLoader();
|
const redmineStatusesConfig = RedmineStatusesConfigLoader();
|
||||||
const redmineStatusChanges = RedmineStatusChangesConfigLoader();
|
const redmineStatusChanges = RedmineStatusChangesConfigLoader();
|
||||||
const redmineEccm = RedmineEccmConfig();
|
const redmineEccm = RedmineEccmConfig();
|
||||||
const redmineCurrentUserRules = RedmineCurrentUserRulesConfig();
|
const redmineCurrentUserRules = RedmineCurrentUserRulesConfig();
|
||||||
|
const simpleKanbanBoard = SimpleKanbanBoardConfig();
|
||||||
|
|
||||||
let appConfig: AppConfig;
|
let appConfig: AppConfig;
|
||||||
|
|
||||||
|
|
@ -36,6 +38,7 @@ export default (): AppConfig => {
|
||||||
redmineStatusChanges: redmineStatusChanges,
|
redmineStatusChanges: redmineStatusChanges,
|
||||||
redmineEccm: redmineEccm,
|
redmineEccm: redmineEccm,
|
||||||
redmineCurrentUserRules: redmineCurrentUserRules,
|
redmineCurrentUserRules: redmineCurrentUserRules,
|
||||||
|
simpleKanbanBoard: simpleKanbanBoard,
|
||||||
};
|
};
|
||||||
|
|
||||||
return appConfig;
|
return appConfig;
|
||||||
|
|
|
||||||
27
src/configs/simple-kanban-board.config.ts
Normal file
27
src/configs/simple-kanban-board.config.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { parse } from 'jsonc-parser';
|
||||||
|
|
||||||
|
export type SimpleKanbanBoardConfig = {
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let config: SimpleKanbanBoardConfig;
|
||||||
|
|
||||||
|
export default (): SimpleKanbanBoardConfig => {
|
||||||
|
if (config) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDefinedConfigPath =
|
||||||
|
process.env['ELTEX_REDMINE_HELPER_KANBAN_SIMPLE_BOARD_CONFIG_PATH'];
|
||||||
|
const defaultConfigPath = join('configs', 'simple-kanban-board-config.jsonc');
|
||||||
|
|
||||||
|
const configPath = userDefinedConfigPath || defaultConfigPath;
|
||||||
|
|
||||||
|
const rawData = readFileSync(configPath, { encoding: 'utf-8' });
|
||||||
|
|
||||||
|
config = parse(rawData);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { RootIssueSubTreesWidgetService } from '@app/event-emitter/project-dashboard/widgets/root-issue-subtrees.widget.service';
|
|
||||||
import { Controller, Get } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Controller('eccm-1.10-dashboard')
|
|
||||||
export class Eccm110DashboardController {
|
|
||||||
constructor(
|
|
||||||
private rootIssueSubTreesWidgetService: RootIssueSubTreesWidgetService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
// TODO: code for Eccm110DashboardController
|
|
||||||
|
|
||||||
@Get('/raw')
|
|
||||||
async getRawData(): Promise<any> {
|
|
||||||
return await this.rootIssueSubTreesWidgetService.render({
|
|
||||||
rootIssueId: 2,
|
|
||||||
parentsAsGroups: true,
|
|
||||||
statuses: [
|
|
||||||
'New',
|
|
||||||
'Closed',
|
|
||||||
'In Progress',
|
|
||||||
'Re-opened',
|
|
||||||
'Code Review',
|
|
||||||
'Resolved',
|
|
||||||
'Testing',
|
|
||||||
'Wait Release',
|
|
||||||
'Pending',
|
|
||||||
'Feedback',
|
|
||||||
'Rejected',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
34
src/dashboards/simple-kanban-board.controller.ts
Normal file
34
src/dashboards/simple-kanban-board.controller.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader';
|
||||||
|
import { RootIssueSubTreesWidgetService } from '@app/event-emitter/project-dashboard/widgets/root-issue-subtrees.widget.service';
|
||||||
|
import { Controller, Get, Param, Render } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { parse } from 'jsonc-parser';
|
||||||
|
|
||||||
|
@Controller('simple-kanban-board')
|
||||||
|
export class SimpleKanbanBoardController {
|
||||||
|
private path: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private rootIssueSubTreesWidgetService: RootIssueSubTreesWidgetService,
|
||||||
|
private dynamicLoader: DynamicLoader,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {
|
||||||
|
this.path = this.configService.get<string>('simpleKanbanBoard.path');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/tree/:name/raw')
|
||||||
|
async getRawData(@Param('name') name: string): Promise<any> {
|
||||||
|
const cfg = this.dynamicLoader.load(name, {
|
||||||
|
path: this.path,
|
||||||
|
ext: 'jsonc',
|
||||||
|
parser: parse,
|
||||||
|
});
|
||||||
|
return await this.rootIssueSubTreesWidgetService.render(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/tree/:name')
|
||||||
|
@Render('simple-kanban-board')
|
||||||
|
async get(@Param('name') name: string): Promise<any> {
|
||||||
|
return await this.getRawData(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { MainConfigModel } from '@app/event-emitter/models/main-config-model';
|
import { MainConfigModel } from '@app/event-emitter/models/main-config-model';
|
||||||
|
import { SimpleKanbanBoardConfig } from 'src/configs/simple-kanban-board.config';
|
||||||
import { EccmConfig } from './eccm-config.model';
|
import { EccmConfig } from './eccm-config.model';
|
||||||
import { StatusChangesConfig } from './status-changes-config.model';
|
import { StatusChangesConfig } from './status-changes-config.model';
|
||||||
import { StatusesConfig } from './statuses-config.model';
|
import { StatusesConfig } from './statuses-config.model';
|
||||||
|
|
@ -9,6 +10,7 @@ export type AppConfig = {
|
||||||
redmineStatusChanges: StatusChangesConfig.Config;
|
redmineStatusChanges: StatusChangesConfig.Config;
|
||||||
redmineEccm: EccmConfig.Config;
|
redmineEccm: EccmConfig.Config;
|
||||||
redmineCurrentUserRules: Record<string, string>;
|
redmineCurrentUserRules: Record<string, string>;
|
||||||
|
simpleKanbanBoard: SimpleKanbanBoardConfig;
|
||||||
couchDb: {
|
couchDb: {
|
||||||
dbs: {
|
dbs: {
|
||||||
changes: string;
|
changes: string;
|
||||||
|
|
|
||||||
66
views/simple-kanban-board.hbs
Normal file
66
views/simple-kanban-board.hbs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>ECCM 1.10 Kanban board</title>
|
||||||
|
<style>
|
||||||
|
.kanban-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.kanban-column {
|
||||||
|
width: 200px;
|
||||||
|
background-color: rgb(225, 225, 225);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.kanban-header {
|
||||||
|
text-align: center;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.kanban-card {
|
||||||
|
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-color: black;*/
|
||||||
|
border-style: solid;
|
||||||
|
margin: 2px;
|
||||||
|
padding: 3px;
|
||||||
|
width: 190px;
|
||||||
|
/*display: flex;*/
|
||||||
|
box-shadow: 0 0 3px rgba(0,0,0,0.5);
|
||||||
|
border-radius: 3px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.kanban-card div {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
.kanban-card .kanban-card-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{{#each this}}
|
||||||
|
<h1>{{this.metainfo.title}}</h1>
|
||||||
|
<div class="kanban-container">
|
||||||
|
{{#each this.data}}
|
||||||
|
<div class="kanban-column">
|
||||||
|
<div class="kanban-header">{{this.status}}</div>
|
||||||
|
{{#each this.issues}}
|
||||||
|
<div class="kanban-card">
|
||||||
|
<div class="kanban-card-title">{{this.tracker.name}} #{{this.id}} - {{this.subject}}</div>
|
||||||
|
<div>Исп.: {{this.current_user.name}}</div>
|
||||||
|
<div>Прогресс: {{this.done_ration}}</div>
|
||||||
|
<div>Трудозатраты: {{this.total_spent_hours}} / {{this.total_estimated_hours}}</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue