@basementuniverse/kanbn 2.0.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/coverage/tmp/coverage-916017-1788028598821-0.json +1 -0
- package/coverage/tmp/{coverage-214293-1787777184569-0.json → coverage-916018-1788028597179-0.json} +1 -1
- package/coverage/tmp/coverage-916036-1788028598796-0.json +1 -0
- package/docs/actions.md +337 -0
- package/docs/advanced-configuration.md +55 -0
- package/docs/commands/add.txt +17 -1
- package/docs/commands/archive.txt +11 -0
- package/docs/commands/board.txt +9 -0
- package/docs/commands/boards.txt +34 -0
- package/docs/commands/burndown.txt +7 -0
- package/docs/commands/comment.txt +13 -1
- package/docs/commands/contributors.txt +58 -0
- package/docs/commands/edit.txt +18 -1
- package/docs/commands/find.txt +39 -1
- package/docs/commands/gantt.txt +6 -0
- package/docs/commands/help.txt +2 -0
- package/docs/commands/history.txt +6 -0
- package/docs/commands/init.txt +13 -0
- package/docs/commands/move.txt +26 -0
- package/docs/commands/remove.txt +22 -1
- package/docs/commands/rename.txt +5 -0
- package/docs/commands/restore.txt +12 -0
- package/docs/commands/sort.txt +23 -0
- package/docs/commands/sprint.txt +9 -0
- package/docs/commands/status.txt +10 -1
- package/docs/commands/task.txt +9 -0
- package/docs/commands/validate.txt +29 -1
- package/docs/contributors.md +145 -0
- package/docs/filtering-and-sorting.md +60 -3
- package/docs/index-structure.md +145 -11
- package/docs/index.md +6 -2
- package/docs/multiple-boards.md +259 -0
- package/docs/quick-start.md +21 -1
- package/docs/task-structure.md +31 -3
- package/example/README.md +23 -0
- package/example/advanced/kanbn.yml +65 -0
- package/example/boards/.kanbn/design.md +31 -0
- package/example/boards/.kanbn/index.md +44 -0
- package/example/boards/.kanbn/tasks/add-usage-alert-emails.md +19 -0
- package/example/boards/.kanbn/tasks/build-tenant-settings-page.md +43 -0
- package/example/boards/.kanbn/tasks/create-organization-switcher.md +44 -0
- package/example/boards/.kanbn/tasks/design-onboarding-checklist.md +22 -0
- package/example/boards/.kanbn/tasks/refresh-marketing-site.md +28 -0
- package/example/boards/.kanbn/tasks/ship-billing-portal.md +29 -0
- package/package.json +9 -7
- package/routes/add.json +40 -12
- package/routes/archive.json +14 -2
- package/routes/board.json +11 -3
- package/routes/boards.json +30 -0
- package/routes/burndown.json +23 -7
- package/routes/comment.json +19 -5
- package/routes/contributors.json +18 -0
- package/routes/edit.json +37 -11
- package/routes/find.json +37 -12
- package/routes/gantt.json +20 -6
- package/routes/history.json +39 -25
- package/routes/init.json +3 -1
- package/routes/move.json +24 -6
- package/routes/remove.json +18 -3
- package/routes/rename.json +11 -3
- package/routes/restore.json +14 -2
- package/routes/sort.json +40 -11
- package/routes/sprint.json +14 -4
- package/routes/status.json +23 -7
- package/routes/task.json +10 -2
- package/routes/validate.json +18 -5
- package/skills/kanbn-plan/SKILL.md +10 -1
- package/skills/kanbn-replan/SKILL.md +6 -1
- package/src/actions.js +904 -0
- package/src/board.js +25 -2
- package/src/controller/add.js +72 -55
- package/src/controller/archive.js +9 -4
- package/src/controller/board.js +21 -13
- package/src/controller/boards.js +140 -0
- package/src/controller/burndown.js +14 -7
- package/src/controller/comment.js +13 -6
- package/src/controller/contributors.js +166 -0
- package/src/controller/edit.js +48 -18
- package/src/controller/find.js +87 -13
- package/src/controller/gantt.js +13 -6
- package/src/controller/history.js +13 -6
- package/src/controller/init.js +39 -4
- package/src/controller/move.js +153 -25
- package/src/controller/remove.js +73 -13
- package/src/controller/rename.js +8 -4
- package/src/controller/restore.js +24 -8
- package/src/controller/sort.js +59 -3
- package/src/controller/sprint.js +31 -7
- package/src/controller/status.js +8 -4
- package/src/controller/task.js +34 -10
- package/src/controller/validate.js +197 -7
- package/src/git-user-name.js +9 -0
- package/src/git-user.js +55 -0
- package/src/main.d.ts +387 -9
- package/src/main.js +2757 -113
- package/src/parse-index.js +219 -17
- package/src/parse-task.js +16 -0
- package/src/utility.js +274 -0
- package/coverage/tmp/coverage-214292-1787777191526-0.json +0 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const workspace = require('../main');
|
|
2
|
+
const utility = require('../utility');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Show the workspace's contributors
|
|
6
|
+
* @param {object[]} contributors
|
|
7
|
+
* @param {?string} currentUser
|
|
8
|
+
* @param {boolean} json
|
|
9
|
+
*/
|
|
10
|
+
function showContributors(contributors, currentUser, json) {
|
|
11
|
+
if (json) {
|
|
12
|
+
console.log(JSON.stringify({ contributors, currentUser }, null, 2));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (!contributors.length) {
|
|
16
|
+
console.log('No contributors are declared in this workspace');
|
|
17
|
+
console.log(utility.replaceTags(
|
|
18
|
+
`\n{d}Add a {d}{b}contributors{b}{d} list to your config file or the main board's front matter -{d} ` +
|
|
19
|
+
`{b}kanbn contributors --help{b}`
|
|
20
|
+
));
|
|
21
|
+
showCurrentUser(currentUser);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
console.log(`${contributors.length} ${contributors.length === 1 ? 'contributor' : 'contributors'}:`);
|
|
25
|
+
for (const contributor of contributors) {
|
|
26
|
+
console.log(utility.replaceTags(
|
|
27
|
+
`\n{b}${contributor.name}{b}` +
|
|
28
|
+
(contributor.name === currentUser ? ' (you)' : '')
|
|
29
|
+
));
|
|
30
|
+
if (contributor.displayName !== contributor.name) {
|
|
31
|
+
console.log(` ${contributor.displayName}`);
|
|
32
|
+
}
|
|
33
|
+
if (contributor.email) {
|
|
34
|
+
console.log(` ${contributor.email}`);
|
|
35
|
+
}
|
|
36
|
+
if (contributor.aliases.length) {
|
|
37
|
+
console.log(` also known as ${contributor.aliases.map(alias => `"${alias}"`).join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
if (contributor.colour) {
|
|
40
|
+
console.log(` ${contributor.colour}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
showCurrentUser(currentUser, contributors);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Say who kanbn thinks the current user is, since that's what a bare --assigned and "@me" resolve to
|
|
48
|
+
* @param {?string} currentUser
|
|
49
|
+
* @param {object[]} [contributors=[]]
|
|
50
|
+
*/
|
|
51
|
+
function showCurrentUser(currentUser, contributors = []) {
|
|
52
|
+
if (currentUser === null) {
|
|
53
|
+
console.log(utility.replaceTags(
|
|
54
|
+
`\n{d}Kanbn can't work out who you are. Set {d}{b}KANBN_USER{b}{d}, or set a git username with{d} ` +
|
|
55
|
+
`{b}git config user.name "your name"{b}`
|
|
56
|
+
));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (contributors.length && !contributors.some(contributor => contributor.name === currentUser)) {
|
|
60
|
+
console.log(utility.replaceTags(
|
|
61
|
+
`\n{d}You are{d} {b}${currentUser}{b}{d}, which isn't in the contributors list{d}`
|
|
62
|
+
));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.log(utility.replaceTags(`\n{d}You are{d} {b}${currentUser}{b}`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Show how contributors are used across the workspace's tasks, and which names are in use that the
|
|
70
|
+
* contributors list doesn't know about
|
|
71
|
+
* @param {object} usage
|
|
72
|
+
* @param {?string} currentUser
|
|
73
|
+
* @param {boolean} json
|
|
74
|
+
*/
|
|
75
|
+
function showUsage(usage, currentUser, json) {
|
|
76
|
+
if (json) {
|
|
77
|
+
console.log(JSON.stringify({ ...usage, currentUser }, null, 2));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (usage.contributors.length) {
|
|
81
|
+
console.log(`${usage.contributors.length} ${usage.contributors.length === 1 ? 'contributor' : 'contributors'}:`);
|
|
82
|
+
for (const contributor of usage.contributors) {
|
|
83
|
+
console.log(utility.replaceTags(
|
|
84
|
+
`\n{b}${contributor.name}{b}` +
|
|
85
|
+
(contributor.name === currentUser ? ' (you)' : '')
|
|
86
|
+
));
|
|
87
|
+
console.log(
|
|
88
|
+
` ${contributor.tasks} ${contributor.tasks === 1 ? 'task' : 'tasks'}, ` +
|
|
89
|
+
`assigned ${contributor.assigned}, ` +
|
|
90
|
+
`${contributor.comments} ${contributor.comments === 1 ? 'comment' : 'comments'}`
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// A contributor matched through an alias or a different case is worth showing: it's a spelling
|
|
94
|
+
// that works today only because the alias is there to catch it
|
|
95
|
+
for (const spelling of contributor.spellings.filter(s => s.value !== contributor.name)) {
|
|
96
|
+
console.log(` written as "${spelling.value}" in ${spelling.tasks.length} ${spelling.tasks.length === 1 ? 'task' : 'tasks'}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
console.log('No contributors are declared in this workspace');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!usage.unknown.length) {
|
|
104
|
+
console.log(
|
|
105
|
+
usage.contributors.length
|
|
106
|
+
? '\nEvery name used in a task matches a contributor'
|
|
107
|
+
: '\nNo names are used in any task'
|
|
108
|
+
);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log(
|
|
112
|
+
`\n${usage.unknown.length} ${usage.unknown.length === 1 ? 'name is' : 'names are'} used in tasks ` +
|
|
113
|
+
`but ${usage.unknown.length === 1 ? "isn't" : "aren't"} a known contributor:`
|
|
114
|
+
);
|
|
115
|
+
for (const unknown of usage.unknown) {
|
|
116
|
+
console.log(utility.replaceTags(
|
|
117
|
+
`\n{b}${unknown.value}{b}` +
|
|
118
|
+
(unknown.value === currentUser ? ' (you)' : '')
|
|
119
|
+
));
|
|
120
|
+
console.log(
|
|
121
|
+
` ${unknown.tasks.length} ${unknown.tasks.length === 1 ? 'task' : 'tasks'}, ` +
|
|
122
|
+
`assigned ${unknown.assigned}, ` +
|
|
123
|
+
`${unknown.comments} ${unknown.comments === 1 ? 'comment' : 'comments'}`
|
|
124
|
+
);
|
|
125
|
+
console.log(` ${unknown.tasks.join(', ')}`);
|
|
126
|
+
}
|
|
127
|
+
console.log(utility.replaceTags(
|
|
128
|
+
`\n{d}Add these to {d}{b}contributors{b}{d}, or as an {d}{b}alias{b}{d} of a contributor already ` +
|
|
129
|
+
`in the list. Nothing is rewritten either way - {d}{b}assigned{b}{d} and comment {d}{b}author{b}` +
|
|
130
|
+
`{d} are free text{d}`
|
|
131
|
+
));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = async args => {
|
|
135
|
+
|
|
136
|
+
// Contributors are workspace-scoped, so this command isn't board-scoped either
|
|
137
|
+
if (!await workspace.workspaceInitialised()) {
|
|
138
|
+
utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let currentUser;
|
|
143
|
+
try {
|
|
144
|
+
currentUser = await workspace.currentUser();
|
|
145
|
+
} catch (error) {
|
|
146
|
+
utility.error(error);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Show how the contributors are actually used across the workspace's tasks
|
|
151
|
+
if (args.usage) {
|
|
152
|
+
try {
|
|
153
|
+
showUsage(await workspace.getContributorUsage(), currentUser, args.json);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
utility.error(error);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// List contributors
|
|
161
|
+
try {
|
|
162
|
+
showContributors(await workspace.getContributors(), currentUser, args.json);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
utility.error(error);
|
|
165
|
+
}
|
|
166
|
+
};
|
package/src/controller/edit.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
const
|
|
1
|
+
const workspace = require('../main');
|
|
2
2
|
const utility = require('../utility');
|
|
3
3
|
const inquirer = require('inquirer');
|
|
4
4
|
const fuzzy = require('fuzzy');
|
|
5
5
|
const chrono = require('chrono-node');
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
// The board this command targets - set from --board before anything else runs
|
|
8
|
+
let kanbn = workspace;
|
|
7
9
|
|
|
8
10
|
inquirer.registerPrompt('datepicker', require('inquirer-datepicker'));
|
|
9
11
|
inquirer.registerPrompt('recursive', require('inquirer-recursive'));
|
|
@@ -15,9 +17,11 @@ inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt'))
|
|
|
15
17
|
* @param {string[]} taskIds
|
|
16
18
|
* @param {string} columnName
|
|
17
19
|
* @param {string[]} columnNames
|
|
20
|
+
* @param {?string} currentUser
|
|
21
|
+
* @param {object[]} contributors
|
|
18
22
|
* @return {Promise<any>}
|
|
19
23
|
*/
|
|
20
|
-
async function interactive(taskData, taskIds, columnName, columnNames) {
|
|
24
|
+
async function interactive(taskData, taskIds, columnName, columnNames, currentUser, contributors) {
|
|
21
25
|
const dueDateExists = (
|
|
22
26
|
'metadata' in taskData &&
|
|
23
27
|
'due' in taskData.metadata &&
|
|
@@ -223,13 +227,12 @@ async function interactive(taskData, taskIds, columnName, columnNames) {
|
|
|
223
227
|
default: false,
|
|
224
228
|
when: answers => !assignedExists
|
|
225
229
|
},
|
|
226
|
-
{
|
|
227
|
-
type: 'input',
|
|
230
|
+
utility.assignedPrompt({
|
|
228
231
|
name: 'assigned',
|
|
229
232
|
message: 'Assigned to:',
|
|
230
|
-
default: assignedExists ? taskData.metadata.assigned :
|
|
233
|
+
default: assignedExists ? taskData.metadata.assigned : (currentUser || ''),
|
|
231
234
|
when: answers => answers.setAssigned || answers.editAssigned === 'edit'
|
|
232
|
-
},
|
|
235
|
+
}, contributors),
|
|
233
236
|
{
|
|
234
237
|
type: 'recursive',
|
|
235
238
|
name: 'addSubTasks',
|
|
@@ -447,6 +450,7 @@ function updateTask(taskId, taskData, columnName, unsetFields = []) {
|
|
|
447
450
|
.updateTask(taskId, taskData, columnName, unsetFields)
|
|
448
451
|
.then(taskId => {
|
|
449
452
|
console.log(`Updated task "${taskId}"`);
|
|
453
|
+
utility.showActionWarnings(kanbn);
|
|
450
454
|
})
|
|
451
455
|
.catch(error => {
|
|
452
456
|
utility.error(error);
|
|
@@ -455,25 +459,42 @@ function updateTask(taskId, taskData, columnName, unsetFields = []) {
|
|
|
455
459
|
|
|
456
460
|
module.exports = async args => {
|
|
457
461
|
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
462
|
+
// Work out which board this command targets
|
|
463
|
+
const scoped = await utility.resolveBoard(workspace, args);
|
|
464
|
+
if (scoped === null) {
|
|
461
465
|
return;
|
|
462
466
|
}
|
|
467
|
+
kanbn = scoped;
|
|
463
468
|
|
|
464
469
|
// Get the task that we're editing
|
|
465
|
-
|
|
470
|
+
let taskId = args._[1];
|
|
466
471
|
if (!taskId) {
|
|
467
472
|
utility.error('No task id specified\nTry running {b}kanbn edit "task id"{b}');
|
|
468
473
|
return;
|
|
469
474
|
}
|
|
470
475
|
|
|
471
|
-
// Make sure the task exists
|
|
476
|
+
// Make sure the task exists. Editing a simple task is how it becomes a real one - there is nothing
|
|
477
|
+
// to edit until it has a file - so promote it first and then edit the task that produces
|
|
472
478
|
try {
|
|
473
479
|
await kanbn.taskExists(taskId);
|
|
474
480
|
} catch (error) {
|
|
475
|
-
|
|
476
|
-
|
|
481
|
+
if (await kanbn.taskFileExists(taskId)) {
|
|
482
|
+
utility.error(error);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const simpleTask = await utility.resolveSimpleTask(kanbn, taskId, error);
|
|
486
|
+
if (simpleTask === null) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
taskId = await kanbn.promoteSimpleTask(taskId);
|
|
491
|
+
console.log(utility.replaceTags(
|
|
492
|
+
`{d}Promoted simple task "${simpleTask.text}" to task "${taskId}"{d}`
|
|
493
|
+
));
|
|
494
|
+
} catch (promoteError) {
|
|
495
|
+
utility.error(promoteError);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
477
498
|
}
|
|
478
499
|
|
|
479
500
|
// Get the index and make sure it has some columns
|
|
@@ -578,10 +599,12 @@ module.exports = async args => {
|
|
|
578
599
|
if (!('metadata' in taskData)) {
|
|
579
600
|
taskData.metadata = {};
|
|
580
601
|
}
|
|
581
|
-
const gitUsername = getGitUsername();
|
|
582
602
|
if (args.assigned === true) {
|
|
583
|
-
|
|
584
|
-
|
|
603
|
+
|
|
604
|
+
// A bare --assigned means "me", canonicalised against the contributors list if there is one
|
|
605
|
+
const currentUser = await kanbn.currentUser();
|
|
606
|
+
if (currentUser) {
|
|
607
|
+
taskData.metadata.assigned = currentUser;
|
|
585
608
|
}
|
|
586
609
|
} else {
|
|
587
610
|
taskData.metadata.assigned = utility.strArg(args.assigned);
|
|
@@ -809,7 +832,14 @@ module.exports = async args => {
|
|
|
809
832
|
|
|
810
833
|
// Update task interactively
|
|
811
834
|
if (args.interactive) {
|
|
812
|
-
interactive(
|
|
835
|
+
interactive(
|
|
836
|
+
taskData,
|
|
837
|
+
taskIds,
|
|
838
|
+
columnName,
|
|
839
|
+
columnNames,
|
|
840
|
+
await kanbn.currentUser(),
|
|
841
|
+
await kanbn.getContributors()
|
|
842
|
+
)
|
|
813
843
|
.then(answers => {
|
|
814
844
|
|
|
815
845
|
// Name
|
package/src/controller/find.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
const
|
|
1
|
+
const workspace = require('../main');
|
|
2
2
|
const utility = require('../utility');
|
|
3
3
|
const inquirer = require('inquirer');
|
|
4
4
|
const chrono = require('chrono-node');
|
|
5
5
|
const yaml = require('yamljs');
|
|
6
6
|
|
|
7
|
+
// The board this command targets - set from --board before anything else runs
|
|
8
|
+
let kanbn = workspace;
|
|
9
|
+
|
|
7
10
|
inquirer.registerPrompt('recursive', require('inquirer-recursive'));
|
|
8
11
|
|
|
9
12
|
const searchFields = [
|
|
@@ -111,6 +114,31 @@ const searchFields = [
|
|
|
111
114
|
name: 'Assigned',
|
|
112
115
|
field: 'assigned',
|
|
113
116
|
type: 'string'
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'Overdue',
|
|
120
|
+
field: 'overdue',
|
|
121
|
+
type: 'boolean'
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'Is started',
|
|
125
|
+
field: 'is-started',
|
|
126
|
+
type: 'boolean'
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'Is completed',
|
|
130
|
+
field: 'is-completed',
|
|
131
|
+
type: 'boolean'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'In started column',
|
|
135
|
+
field: 'in-started-column',
|
|
136
|
+
type: 'boolean'
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'In completed column',
|
|
140
|
+
field: 'in-completed-column',
|
|
141
|
+
type: 'boolean'
|
|
114
142
|
}
|
|
115
143
|
];
|
|
116
144
|
|
|
@@ -213,12 +241,11 @@ async function interactive() {
|
|
|
213
241
|
* @param {boolean} quiet
|
|
214
242
|
* @param {boolean} json
|
|
215
243
|
*/
|
|
216
|
-
function findTasks(filters, quiet, json) {
|
|
244
|
+
function findTasks(filters, quiet, json, allBoards = false) {
|
|
217
245
|
const removeEmptyProperties = o => Object.fromEntries(Object.entries(o).filter(
|
|
218
246
|
([k, v]) => !(Array.isArray(v) && v.length == 0) && !!v
|
|
219
247
|
));
|
|
220
|
-
kanbn
|
|
221
|
-
.search(filters, quiet)
|
|
248
|
+
(allBoards ? kanbn.searchAllBoards(filters, quiet) : kanbn.search(filters, quiet))
|
|
222
249
|
.then(results => {
|
|
223
250
|
if (quiet) {
|
|
224
251
|
console.log(json ? JSON.stringify(results, null, 2) : results.join('\n'));
|
|
@@ -282,6 +309,24 @@ function convertNumericFilters(filters, filterName) {
|
|
|
282
309
|
return true;
|
|
283
310
|
}
|
|
284
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Check that a filter holds a boolean value. Repeating a boolean option is meaningless, so the last
|
|
314
|
+
* value wins
|
|
315
|
+
* @param {object} filters The current filter object
|
|
316
|
+
* @param {string} filterName The property name for the filter
|
|
317
|
+
* @return {boolean} True if the value is a boolean
|
|
318
|
+
*/
|
|
319
|
+
function convertBooleanFilters(filters, filterName) {
|
|
320
|
+
const value = Array.isArray(filters[filterName])
|
|
321
|
+
? filters[filterName][filters[filterName].length - 1]
|
|
322
|
+
: filters[filterName];
|
|
323
|
+
if (typeof value !== 'boolean') {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
filters[filterName] = value;
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
285
330
|
/**
|
|
286
331
|
* Convert a filter or array of filters to date values
|
|
287
332
|
* @param {object} filters The current filter object
|
|
@@ -309,11 +354,12 @@ function convertDateFilters(filters, filterName) {
|
|
|
309
354
|
|
|
310
355
|
module.exports = async args => {
|
|
311
356
|
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
357
|
+
// Work out which board this command targets
|
|
358
|
+
const scoped = await utility.resolveBoard(workspace, args);
|
|
359
|
+
if (scoped === null) {
|
|
315
360
|
return;
|
|
316
361
|
}
|
|
362
|
+
kanbn = scoped;
|
|
317
363
|
|
|
318
364
|
// Get the index and make sure it has some columns
|
|
319
365
|
let index;
|
|
@@ -340,12 +386,26 @@ module.exports = async args => {
|
|
|
340
386
|
}
|
|
341
387
|
}
|
|
342
388
|
|
|
343
|
-
// Get filters from args
|
|
389
|
+
// Get filters from args. Boolean filters are checked with "in" because false is a filter value in
|
|
390
|
+
// its own right - "--no-overdue" means "not overdue", not "no overdue filter"
|
|
344
391
|
const filters = {};
|
|
345
|
-
for (let
|
|
346
|
-
if (
|
|
347
|
-
|
|
392
|
+
for (let searchField of searchFields) {
|
|
393
|
+
if (searchField.type === 'boolean') {
|
|
394
|
+
if (searchField.field in args) {
|
|
395
|
+
filters[searchField.field] = args[searchField.field];
|
|
396
|
+
}
|
|
397
|
+
} else if (args[searchField.field]) {
|
|
398
|
+
filters[searchField.field] = args[searchField.field];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Expand "@me" in the assigned filter
|
|
403
|
+
if ('assigned' in filters) {
|
|
404
|
+
const assigned = await utility.expandCurrentUser(kanbn, filters.assigned);
|
|
405
|
+
if (assigned === null) {
|
|
406
|
+
return;
|
|
348
407
|
}
|
|
408
|
+
filters.assigned = assigned;
|
|
349
409
|
}
|
|
350
410
|
|
|
351
411
|
// Check and convert numeric filters
|
|
@@ -386,6 +446,20 @@ module.exports = async args => {
|
|
|
386
446
|
}
|
|
387
447
|
}
|
|
388
448
|
|
|
449
|
+
// Check computed boolean filters
|
|
450
|
+
for (const [filterName, label] of [
|
|
451
|
+
['overdue', 'Overdue'],
|
|
452
|
+
['is-started', 'Is started'],
|
|
453
|
+
['is-completed', 'Is completed'],
|
|
454
|
+
['in-started-column', 'In started column'],
|
|
455
|
+
['in-completed-column', 'In completed column']
|
|
456
|
+
]) {
|
|
457
|
+
if (filterName in filters && !convertBooleanFilters(filters, filterName)) {
|
|
458
|
+
utility.error(`${label} filter value must be a boolean`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
389
463
|
// Check date filters
|
|
390
464
|
if ('created' in filters) {
|
|
391
465
|
if (!convertDateFilters(filters, 'created')) {
|
|
@@ -487,7 +561,7 @@ module.exports = async args => {
|
|
|
487
561
|
filter.value
|
|
488
562
|
);
|
|
489
563
|
}
|
|
490
|
-
findTasks(filters, !otherAnswers.nonquiet, otherAnswers.json);
|
|
564
|
+
findTasks(filters, !otherAnswers.nonquiet, otherAnswers.json, !!args['all-boards']);
|
|
491
565
|
})
|
|
492
566
|
.catch(error => {
|
|
493
567
|
utility.error(error);
|
|
@@ -499,6 +573,6 @@ module.exports = async args => {
|
|
|
499
573
|
|
|
500
574
|
// Otherwise create task non-interactively
|
|
501
575
|
} else {
|
|
502
|
-
findTasks(filters, args.quiet, args.json);
|
|
576
|
+
findTasks(filters, args.quiet, args.json, !!args['all-boards']);
|
|
503
577
|
}
|
|
504
578
|
};
|
package/src/controller/gantt.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
const
|
|
1
|
+
const workspace = require('../main');
|
|
2
2
|
const utility = require('../utility');
|
|
3
3
|
const formatDate = require('dateformat');
|
|
4
4
|
const chrono = require('chrono-node');
|
|
5
5
|
|
|
6
|
+
// The board this command targets - set from --board before anything else runs
|
|
7
|
+
let kanbn = workspace;
|
|
8
|
+
|
|
6
9
|
const NOW_MARKER = '┆';
|
|
7
10
|
const ANSI_RESET = '\x1b[0m';
|
|
8
11
|
const ANSI_FG_LINE = '\x1b[97m';
|
|
@@ -313,17 +316,21 @@ function truncateName(name, maxWidth) {
|
|
|
313
316
|
|
|
314
317
|
module.exports = async args => {
|
|
315
318
|
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
+
// Work out which board this command targets
|
|
320
|
+
const scoped = await utility.resolveBoard(workspace, args);
|
|
321
|
+
if (scoped === null) {
|
|
319
322
|
return;
|
|
320
323
|
}
|
|
324
|
+
kanbn = scoped;
|
|
321
325
|
const index = await kanbn.getIndex();
|
|
322
326
|
|
|
323
|
-
// Get assigned
|
|
327
|
+
// Get assigned, expanding "@me" to the current user
|
|
324
328
|
let assigned = null;
|
|
325
329
|
if (args.assigned) {
|
|
326
|
-
assigned = utility.strArg(args.assigned);
|
|
330
|
+
assigned = await utility.expandCurrentUser(kanbn, utility.strArg(args.assigned));
|
|
331
|
+
if (assigned === null) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
327
334
|
}
|
|
328
335
|
|
|
329
336
|
// Get columns
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
const
|
|
1
|
+
const workspace = require('../main');
|
|
2
2
|
const utility = require('../utility');
|
|
3
3
|
const chrono = require('chrono-node');
|
|
4
4
|
const formatDate = require('dateformat');
|
|
5
5
|
|
|
6
|
+
// The board this command targets - set from --board before anything else runs
|
|
7
|
+
let kanbn = workspace;
|
|
8
|
+
|
|
6
9
|
function formatHistoryEvent(event) {
|
|
7
10
|
const details = [];
|
|
8
11
|
if ('column' in event.event) {
|
|
@@ -29,11 +32,12 @@ function formatMetadataEvent(event, dateFormat) {
|
|
|
29
32
|
|
|
30
33
|
module.exports = async args => {
|
|
31
34
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
// Work out which board this command targets
|
|
36
|
+
const scoped = await utility.resolveBoard(workspace, args);
|
|
37
|
+
if (scoped === null) {
|
|
35
38
|
return;
|
|
36
39
|
}
|
|
40
|
+
kanbn = scoped;
|
|
37
41
|
const index = await kanbn.getIndex();
|
|
38
42
|
|
|
39
43
|
// Get sprint numbers or names
|
|
@@ -61,10 +65,13 @@ module.exports = async args => {
|
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
// Get assigned
|
|
68
|
+
// Get assigned, expanding "@me" to the current user
|
|
65
69
|
let assigned = null;
|
|
66
70
|
if (args.assigned) {
|
|
67
|
-
assigned = utility.strArg(args.assigned);
|
|
71
|
+
assigned = await utility.expandCurrentUser(kanbn, utility.strArg(args.assigned));
|
|
72
|
+
if (assigned === null) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
// Get task ids
|
package/src/controller/init.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
const kanbn_module = require('../main');
|
|
2
|
-
const
|
|
2
|
+
const workspace = new kanbn_module.Kanbn();
|
|
3
3
|
const utility = require('../utility');
|
|
4
4
|
const inquirer = require('inquirer');
|
|
5
5
|
|
|
6
|
+
// The board being initialised - set from --board before anything else runs
|
|
7
|
+
let kanbn = workspace;
|
|
8
|
+
|
|
6
9
|
inquirer.registerPrompt('recursive', require('inquirer-recursive'));
|
|
7
10
|
|
|
8
11
|
/**
|
|
@@ -73,11 +76,20 @@ async function interactive(options, initialised) {
|
|
|
73
76
|
* Initialise kanbn
|
|
74
77
|
* @param {object} options
|
|
75
78
|
* @param {boolean} initialised
|
|
79
|
+
* @param {?string} boardSlug The slug of the board being initialised, or null for the main board
|
|
76
80
|
*/
|
|
77
|
-
async function initialise(options, initialised) {
|
|
81
|
+
async function initialise(options, initialised, boardSlug = null) {
|
|
78
82
|
const mainFolder = await kanbn.getMainFolder();
|
|
79
83
|
kanbn.initialise(options)
|
|
80
84
|
.then(() => {
|
|
85
|
+
if (boardSlug !== null) {
|
|
86
|
+
console.log(
|
|
87
|
+
initialised
|
|
88
|
+
? `Updated board "${boardSlug}" in ${mainFolder}`
|
|
89
|
+
: `Created board "${boardSlug}" in ${mainFolder}`
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
81
93
|
if (initialised) {
|
|
82
94
|
console.log(`Reinitialised existing kanbn board in ${mainFolder}`);
|
|
83
95
|
} else {
|
|
@@ -92,6 +104,29 @@ async function initialise(options, initialised) {
|
|
|
92
104
|
module.exports = async args => {
|
|
93
105
|
let options = {};
|
|
94
106
|
|
|
107
|
+
// Work out which board we're initialising. Without --board this is the main board, i.e. the whole
|
|
108
|
+
// workspace, which is exactly what init has always done
|
|
109
|
+
let boardSlug = null;
|
|
110
|
+
if (args.board) {
|
|
111
|
+
boardSlug = utility.strArg(args.board);
|
|
112
|
+
|
|
113
|
+
// A secondary board lives beside the main board and shares its tasks, so the workspace has to
|
|
114
|
+
// exist first
|
|
115
|
+
if (!await workspace.initialised()) {
|
|
116
|
+
utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
boardSlug = await workspace.validateBoardSlug(boardSlug);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
utility.error(error);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
kanbn = workspace.board(boardSlug);
|
|
126
|
+
} else {
|
|
127
|
+
kanbn = workspace;
|
|
128
|
+
}
|
|
129
|
+
|
|
95
130
|
// If this folder is already initialised, set the default name and description using the current values
|
|
96
131
|
const initialised = await kanbn.initialised();
|
|
97
132
|
if (initialised) {
|
|
@@ -129,7 +164,7 @@ module.exports = async args => {
|
|
|
129
164
|
if ('columns' in answers) {
|
|
130
165
|
answers.columns = answers.columns.map(column => column.columnName);
|
|
131
166
|
}
|
|
132
|
-
await initialise(answers, initialised);
|
|
167
|
+
await initialise(answers, initialised, boardSlug);
|
|
133
168
|
})
|
|
134
169
|
.catch(error => {
|
|
135
170
|
utility.error(error);
|
|
@@ -137,6 +172,6 @@ module.exports = async args => {
|
|
|
137
172
|
|
|
138
173
|
// Non-interactive initialisation
|
|
139
174
|
} else {
|
|
140
|
-
await initialise(options, initialised);
|
|
175
|
+
await initialise(options, initialised, boardSlug);
|
|
141
176
|
}
|
|
142
177
|
};
|