diff --git a/.tpm b/.tpm index 7352305..5385658 100644 --- a/.tpm +++ b/.tpm @@ -9,9 +9,9 @@ team: fte: 1 timeline: v0.11.0: - deadline: 2024-11-21 21:00:00 GMT+0200 + deadline: 2024-11-25 12:00:00 GMT+0200 v0.10.0: - deadline: 2024-11-17 21:00:00 GMT+0200 + deadline: 2024-11-17 23:40:00 GMT+0200 v0.9.0: deadline: 2024-11-10 23:00:00 GMT+0200 v0.8.0: @@ -29,33 +29,34 @@ timeline: v0.2.0: deadline: 2024-09-04 17:30:00 GMT+0200 tasks: | + [-:031:v0.12.0] Fix task output on hierarchy mode @vlad.k [+:030:v0.10.0] Implement deadline class @vlad.k - [-:029:v0.11.0] Implement config command via source class @vlad.k - [-:028:v0.11.0] Tasks spill over batch mode @vlad.k + [-:029:v0.12.0] Implement config command via source class @vlad.k + [-:028:v0.12.0] Tasks spill over batch mode @vlad.k [+:027:v0.10.0] Implement team/member section as classes @vlad.k [+:026:v0.9.0] Implement srs section as classes @vlad.k [+:025:v0.9.0] Show SRS in UI @vlad.k - [-:024:v0.11.0] Tweak task status based on child tasks (normalise) @vlad.k + [-:024:v0.12.0] Tweak task status based on child tasks (normalise) @vlad.k [+:023:v0.9.0] Implement project, team, srs section as classes @vlad.k [+:022:v0.7.0] Describe command - project @vlad.k [+:021:v0.6.0] Add project description section @vlad.k [+:020:v0.5.0] Add option to display tasks with different statuses: --backlog, --dev, --done @vlad.k [+:019:v0.5.0] Add component as optional parameter for ls command @vlad.k - [-:018:v0.11.0] Generate snapshot @vlad.k + [-:018:v0.12.0] Generate snapshot @vlad.k [+:017:v0.5.0] Add SRS section @vlad.k - [-:016:v0.11.0] Add describe command with resolved links @vlad.k + [-:016:v0.12.0] Add describe command with resolved links @vlad.k [x:015:v0.5.0] Integrate .tpm folder at repository root level to store snapshots @vlad.k - [-:014:v0.11.0] Update README.md with Hello World section using repository @vlad.k + [-:014:v0.12.0] Update README.md with Hello World section using repository @vlad.k [+:013:v0.6.0] Add search @vlad.k [+:012:v0.6.0] Add CI/CD @vlad.k [+:011:v0.6.0] Add unit test framework @vlad.k [+:010:v0.6.0] Extract tags, timeline from task description @vlad.k [+:009:v0.5.0] Add command to generate .todo template --team, --timeline, --tasks, --force @vlad.k [+:008:v0.6.0] Merge multiple descriptions from one file @vlad.k - [-:007:v0.11.0] Server command: express server + fs watch @vlad.k + [-:007:v0.12.0] Server command: express server + fs watch @vlad.k [+] Create API using express [+] Implement web part: Dashboard - [-] Implement web part: Timeline + [>] Implement web part: Timeline [-] Implement web part: Team [-] Add --watch option to watch for changes in .todo file and refresh tpm internal state [+:006:v0.4.0] Extract status, id, assingees from task description @vlad.k diff --git a/cli.js b/cli.js index 3cac4c7..9f197b1 100755 --- a/cli.js +++ b/cli.js @@ -10,6 +10,7 @@ const findUp = require('find-up') const yargs = require('yargs/yargs') const { hideBin } = require('yargs/helpers'); const yaml = require('yaml'); +const { dump } = require('js-yaml'); const getApp = async (argv, load, fn) => { const verbose = argv.verbose; @@ -53,6 +54,8 @@ yargs(hideBin(process.argv)) .option('srs', { describe: 'Include SRS section', default: false, type: 'boolean' }) .option('force', { describe: 'Force command execution', default: false, type: 'boolean' }) + .option('json', { describe: 'Output in json format', default: false, type: 'boolean' }) + .option('yaml', { describe: 'Output in yaml format', default: false, type: 'boolean' }) .option('hierarchy', { describe: 'Output nested components as hierarchy', default: false, type: 'boolean' }) // // ls command aims to work exclusively with tasks @@ -65,13 +68,71 @@ yargs(hideBin(process.argv)) }, async (argv) => { getApp(argv, true, async (a) => { // console.log(argv); - await a.ls({ + const component = await a.ls({ component: argv.component, depth: argv.depth, who: { assignees: argv.assignee, all: argv.all }, - filter: { tag: argv.tag, search: argv.search, deadline: argv.deadline, status: { backlog: argv.backlog, dev: argv.dev, done: argv.done } }, - hierarchy: argv.hierarchy + filter: { tag: argv.tag, search: argv.search, deadline: argv.deadline, status: { backlog: argv.backlog, dev: argv.dev, done: argv.done } } }); + // + const hierarchy = argv.hierarchy; + if (component) { + if (argv.json || argv.yaml) { + if (argv.json) { + a.logger.con(JSON.stringify(component)); + } else { + a.logger.con(yaml.stringify(component)); + } + } else { + const dump = (c, indent, last) => { + // title + let ti = ` `; + let percentage = ''; + if (c.tasks.length) { + percentage = Math.round(c.tasks.reduce((acc, t) => acc + t.percentage, 0) / c.tasks.length); + if (percentage > 0) { + percentage = `(${percentage}%)`; + } else { + percentage = ''; + } + } + if (hierarchy) { + ti = `${indent}` + ((last && c.components.length === 0) ? ' ' : '│') + const title = (c.id) ? `${indent}${last?'└':'├'} ${c.id}` : ''; + a.logger.con(`${title} ${percentage}`); + } else { + if (c.tasks.length) { + a.logger.con(); + a.logger.con(`~ ${c.relativePath} ${percentage}`); + } + } + // tasks + const out = (task, indent) => { + if (task.title) { + const g = task.assignees.length ? ` @(${task.assignees.join(',')})` : ''; + const tg = task.tags.length ? ` #(${task.tags.join(',')})` : ''; + const dl = task.deadline ? ` (${task.deadline})` : ''; + const id = task.id ? ` ${task.id}:` : ''; + a.logger.con(`${indent}${task.status}${id} ${task.title}${g}${tg}${dl}`); + } + // console.log(task); + for (const t of task.tasks) { + out(t, `${indent} `); + } + } + for (const t of c.tasks) { + out(t, ti); + } + // components + const lng = c.components.length; + for (let i = 0; i < lng; i++) { + const lc = (i === lng - 1); + dump(c.components[i], indent + (last? ' ' : '│ '), lc); + } + } + dump(component, '', true); + } + } }); }) // diff --git a/package-lock.json b/package-lock.json index 07b74f7..5ed0314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tln-pm", - "version": "0.10.0", + "version": "0.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tln-pm", - "version": "0.10.0", + "version": "0.11.0", "license": "MIT", "dependencies": { "assign-deep": "^1.0.1", diff --git a/package.json b/package.json index 1e5fc10..a2d3a2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tln-pm", - "version": "0.10.0", + "version": "0.11.0", "description": "Project Management as Code", "main": "cli.js", "scripts": { diff --git a/src/app.js b/src/app.js index afa2ab6..b7f1feb 100644 --- a/src/app.js +++ b/src/app.js @@ -66,7 +66,7 @@ class App { } async ls(options) { - const {component, depth, who, filter, hierarchy} = options; + const {component, depth, who, filter} = options; let aees = [...who.assignees]; // // console.log(options, who.all || aees.length); @@ -84,7 +84,7 @@ class App { if (who.all || aees.length) { const c = await this.getCurrentComponent(component); if (c) { - await c.ls({depth, who: {...who, assignees: aees}, filter, hierarchy, indent: '', last: true}); + return await c.ls({depth, who: {...who, assignees: aees}, filter}); } } } diff --git a/src/component.js b/src/component.js index ee5db82..63f0a70 100644 --- a/src/component.js +++ b/src/component.js @@ -176,10 +176,16 @@ class Component { } async ls(options) { - const {depth, who, filter, hierarchy, indent, last} = options; + const {depth, who, filter} = options; const who2 = { ...who, assignees: await this.getAssignees(who.assignees)}; + // + const ts = { + id: this.id, + relativePath: this.getRelativePath(), + tasks: [], + components: [] + }; // tasks - const ts = { tasks: [] }; for (const task of this.tasks) { const t = await task.filter({who: who2, filter}); //console.log(t); @@ -188,39 +194,20 @@ class Component { } }; // - if (ts && ts.tasks.length) { - let ti = ' '; - if (hierarchy) { - ti = `${indent}${ti}`; - const title = (this.id) ? `${indent}${last?'└':'├'} ${this.id}` : ''; - const summary = '45%'; - this.logger.con(`${title} ${summary}`); - } else { - this.logger.con(); - this.logger.con(`~ ${this.getRelativePath()}`); - } - const out = (task, indent) => { - if (task.title) { - const a = task.assignees.length ? ` @(${task.assignees.join(',')})` : ''; - const tg = task.tags.length ? ` #(${task.tags.join(',')})` : ''; - const dl = task.deadline ? ` (${task.deadline})` : ''; - const id = task.id ? ` ${task.id}:` : ''; - this.logger.con(`${indent}${task.status}${id} ${task.title}${a}${tg}${dl}`); - } - for (const t of task.tasks) { - out(t, `${indent} `); - } - } - out(ts, ''); - } - // about components + // nested components if (depth) { const lng = this.components.length; for (let i = 0; i < lng; i++) { - const lc = (i === lng - 1); - await this.components[i].ls({depth: depth - 1, who: who2, filter, hierarchy, indent: indent + (last? ' ' : '│ '), last: lc}); + const cp = await this.components[i].ls({depth: depth - 1, who: who2, filter}); + if (cp) { + ts.components.push(cp); + } } } + // + if (ts.tasks.length || ts.components.length) { + return ts; + } } async describeProject(options) { diff --git a/src/server.js b/src/server.js index 924f15e..5a34103 100644 --- a/src/server.js +++ b/src/server.js @@ -58,6 +58,14 @@ class Server { root.getTeam(team, true, true) res.send(this.makeResponce(team)); }) + ea.get('/tasks', async(req, res) => { + res.send(this.makeResponce( await app.ls({ + component: null, + depth: 10, + who: { assignees: [], all: true }, + filter: { tag: [], search: [], deadline: [], status: { backlog: true, dev: true, done: false } } + }))); + }) ea.get('/srs', async(req, res) => { res.send(this.makeResponce( await app.describe({ what: { srs: true } }))); }) diff --git a/src/task.js b/src/task.js index 82e030f..91af29c 100644 --- a/src/task.js +++ b/src/task.js @@ -16,7 +16,7 @@ class Task { this.source = source; this.parent = parent; this.indent = indent; - this.status = '-'; + this.status = ''; this.id = null; this.title = ''; this.deadline = ''; @@ -55,7 +55,7 @@ class Task { this.links = links; } - async filter(options, alsoMe = false, alsoTags = false) { + async filter(options, alsoMe = false, alsoTags = false, statusToo = false) { //console.log('filter in', this.id); const {who, filter} = options; // check myself @@ -65,7 +65,7 @@ class Task { { statuses: ['-', '?', '!'], flag: filter.status.backlog }, { statuses: ['>'], flag: filter.status.dev }, { statuses: ['+', 'x'], flag: filter.status.done }, - ].find(v => v.flag && v.statuses.includes(this.status) ); + ].find(v => v.flag && v.statuses.includes(this.status) ) || statusToo; // const tags = [this.deadline].concat(this.tags); const tg = (filter.tag.length ? filter.tag.find( t => tags.includes(t) ) : true) || alsoTags; @@ -73,7 +73,11 @@ class Task { const sr = filter.search.length ? filter.search.find( s => this.title.indexOf(s) >= 0 ) : true; // // console.log(this.id, 'me', me, 'st', st, 'tg', tg, 'sr', sr); - const tasks = (await Promise.all(this.tasks.map(async t => t.filter(options, me, tg)))).filter(v => !!v); + const tasks = (await Promise.all(this.tasks.map(async t => t.filter(options, me, tg, st)))).filter(v => !!v); + let percentage = (this.status === '+' || this.status === 'x') ? 100 : 0; + if (this.tasks.length) { + percentage = Math.round(tasks.reduce((acc, t) => acc + t.percentage, 0) / tasks.length); + } //console.log(this.id, st, who.all, me, tg, sr, tasks.length); if (((who.all || me) && st && tg && sr) || tasks.length) { //console.log('filter out', this.id); @@ -81,6 +85,7 @@ class Task { status: this.status, id: this.id, title: this.title, + percentage, deadline: this.deadline, assignees: this.assignees, tags: this.tags, diff --git a/web/index.html b/web/index.html index 99825d9..56d4def 100644 --- a/web/index.html +++ b/web/index.html @@ -10,6 +10,7 @@ +