@basementuniverse/kanbn 2.1.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 +1 -0
- 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 +32 -0
- package/docs/commands/add.txt +8 -1
- package/docs/commands/archive.txt +6 -0
- package/docs/commands/board.txt +4 -0
- package/docs/commands/burndown.txt +1 -0
- package/docs/commands/comment.txt +8 -1
- package/docs/commands/contributors.txt +58 -0
- package/docs/commands/edit.txt +13 -1
- package/docs/commands/find.txt +28 -1
- package/docs/commands/gantt.txt +1 -0
- package/docs/commands/help.txt +1 -0
- package/docs/commands/history.txt +1 -0
- package/docs/commands/move.txt +17 -0
- package/docs/commands/remove.txt +10 -0
- package/docs/commands/restore.txt +6 -0
- package/docs/commands/sort.txt +18 -0
- package/docs/commands/task.txt +4 -0
- package/docs/commands/validate.txt +14 -1
- package/docs/contributors.md +145 -0
- package/docs/filtering-and-sorting.md +60 -3
- package/docs/index-structure.md +119 -11
- package/docs/index.md +3 -1
- package/docs/multiple-boards.md +1 -0
- package/docs/task-structure.md +9 -2
- package/example/advanced/kanbn.yml +65 -0
- package/package.json +1 -1
- package/routes/add.json +5 -1
- package/routes/archive.json +6 -2
- package/routes/comment.json +5 -1
- package/routes/contributors.json +18 -0
- package/routes/edit.json +5 -1
- package/routes/move.json +4 -2
- package/routes/remove.json +6 -2
- package/routes/restore.json +6 -0
- package/routes/sort.json +5 -0
- package/src/actions.js +904 -0
- package/src/board.js +24 -1
- package/src/controller/add.js +21 -10
- package/src/controller/archive.js +1 -0
- package/src/controller/board.js +13 -4
- package/src/controller/burndown.js +5 -2
- package/src/controller/comment.js +5 -2
- package/src/controller/contributors.js +166 -0
- package/src/controller/edit.js +40 -14
- package/src/controller/find.js +75 -4
- package/src/controller/gantt.js +5 -2
- package/src/controller/history.js +5 -2
- package/src/controller/move.js +84 -10
- package/src/controller/remove.js +39 -2
- package/src/controller/restore.js +1 -0
- package/src/controller/sort.js +40 -0
- package/src/controller/task.js +12 -1
- package/src/controller/validate.js +141 -4
- package/src/git-user-name.js +5 -15
- package/src/git-user.js +55 -0
- package/src/main.d.ts +204 -4
- package/src/main.js +1412 -38
- package/src/parse-index.js +205 -17
- package/src/utility.js +135 -1
- package/coverage/tmp/coverage-214292-1787777191526-0.json +0 -1
package/src/board.js
CHANGED
|
@@ -55,6 +55,23 @@ module.exports = (() => {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Render the simple tasks in a column - lines in the board file that aren't task links. They have
|
|
60
|
+
* no fields to interpolate and take no part in filtering or sorting, so they're shown dimmed at
|
|
61
|
+
* the end of the column, and only in the default layout where the columns on screen are the
|
|
62
|
+
* columns in the file
|
|
63
|
+
* @param {object} index
|
|
64
|
+
* @param {string} columnName
|
|
65
|
+
* @return {string[]} The rendered simple tasks
|
|
66
|
+
*/
|
|
67
|
+
function getSimpleTaskStrings(index, columnName) {
|
|
68
|
+
return ((index.columnContent || {})[columnName] || [])
|
|
69
|
+
.filter(entry => !entry.block && !!entry.text && entry.text.indexOf('\n') === -1)
|
|
70
|
+
|
|
71
|
+
// A caret starts a markup sequence in terminal-kit, so any in the user's own text are escaped
|
|
72
|
+
.map(entry => `^-${entry.text.replace(/\^/g, '^^')}^:`);
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
/**
|
|
59
76
|
* Get a column heading with icons
|
|
60
77
|
* @param {object} index
|
|
@@ -166,7 +183,13 @@ module.exports = (() => {
|
|
|
166
183
|
table.push(board.headings.map(heading => heading.heading));
|
|
167
184
|
board.lanes.forEach(lane => table.push(
|
|
168
185
|
[lane.name],
|
|
169
|
-
lane.columns.map(column
|
|
186
|
+
lane.columns.map((column, i) => {
|
|
187
|
+
const cell = column.map(task => getTaskString(index, task));
|
|
188
|
+
if (view === null) {
|
|
189
|
+
cell.push(...getSimpleTaskStrings(index, board.headings[i].name));
|
|
190
|
+
}
|
|
191
|
+
return cell.join(TASK_SEPARATOR);
|
|
192
|
+
})
|
|
170
193
|
));
|
|
171
194
|
|
|
172
195
|
// Display as a table
|
package/src/controller/add.js
CHANGED
|
@@ -3,7 +3,6 @@ const utility = require('../utility');
|
|
|
3
3
|
const inquirer = require('inquirer');
|
|
4
4
|
const fuzzy = require('fuzzy');
|
|
5
5
|
const chrono = require('chrono-node');
|
|
6
|
-
const getGitUsername = require('../git-user-name');
|
|
7
6
|
|
|
8
7
|
// The boards this command targets - set from --board before anything else runs
|
|
9
8
|
let kanbn = workspace;
|
|
@@ -19,9 +18,11 @@ inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt'))
|
|
|
19
18
|
* @param {string[]} taskIds
|
|
20
19
|
* @param {string} columnName
|
|
21
20
|
* @param {string[]} columnNames
|
|
21
|
+
* @param {?string} currentUser
|
|
22
|
+
* @param {object[]} contributors
|
|
22
23
|
* @return {Promise<any>}
|
|
23
24
|
*/
|
|
24
|
-
async function interactiveCreateTask(taskData, taskIds, columnName, columnNames) {
|
|
25
|
+
async function interactiveCreateTask(taskData, taskIds, columnName, columnNames, currentUser, contributors) {
|
|
25
26
|
const dueDateExists = (
|
|
26
27
|
'metadata' in taskData &&
|
|
27
28
|
'due' in taskData.metadata &&
|
|
@@ -127,13 +128,12 @@ async function interactiveCreateTask(taskData, taskIds, columnName, columnNames)
|
|
|
127
128
|
default: false,
|
|
128
129
|
when: answers => !assignedExists
|
|
129
130
|
},
|
|
130
|
-
{
|
|
131
|
-
type: 'input',
|
|
131
|
+
utility.assignedPrompt({
|
|
132
132
|
name: 'assigned',
|
|
133
133
|
message: 'Assigned to:',
|
|
134
|
-
default: assignedExists ? taskData.metadata.assigned :
|
|
134
|
+
default: assignedExists ? taskData.metadata.assigned : (currentUser || ''),
|
|
135
135
|
when: answers => answers.setAssigned || assignedExists
|
|
136
|
-
},
|
|
136
|
+
}, contributors),
|
|
137
137
|
{
|
|
138
138
|
type: 'recursive',
|
|
139
139
|
name: 'subTasks',
|
|
@@ -250,9 +250,11 @@ async function createTask(taskData, columnName) {
|
|
|
250
250
|
`Created task "${taskId}" in column "${columnName}"` +
|
|
251
251
|
(boardTargets.length > 1 || !first.main ? ` on board "${first.slug}"` : '')
|
|
252
252
|
);
|
|
253
|
+
utility.showActionWarnings(first.kanbn);
|
|
253
254
|
for (const target of rest) {
|
|
254
255
|
await target.kanbn.addTaskToBoard(taskId, target.columnName);
|
|
255
256
|
console.log(`Added task "${taskId}" to column "${target.columnName}" on board "${target.slug}"`);
|
|
257
|
+
utility.showActionWarnings(target.kanbn);
|
|
256
258
|
}
|
|
257
259
|
} catch (error) {
|
|
258
260
|
utility.error(error);
|
|
@@ -400,10 +402,12 @@ module.exports = async args => {
|
|
|
400
402
|
|
|
401
403
|
// Assigned
|
|
402
404
|
if (args.assigned) {
|
|
403
|
-
const gitUsername = getGitUsername();
|
|
404
405
|
if (args.assigned === true) {
|
|
405
|
-
|
|
406
|
-
|
|
406
|
+
|
|
407
|
+
// A bare --assigned means "me", canonicalised against the contributors list if there is one
|
|
408
|
+
const currentUser = await kanbn.currentUser();
|
|
409
|
+
if (currentUser) {
|
|
410
|
+
taskData.metadata.assigned = currentUser;
|
|
407
411
|
}
|
|
408
412
|
} else {
|
|
409
413
|
taskData.metadata.assigned = utility.strArg(args.assigned);
|
|
@@ -508,7 +512,14 @@ module.exports = async args => {
|
|
|
508
512
|
|
|
509
513
|
// Create task interactively
|
|
510
514
|
if (args.interactive) {
|
|
511
|
-
interactiveCreateTask(
|
|
515
|
+
interactiveCreateTask(
|
|
516
|
+
taskData,
|
|
517
|
+
taskIds,
|
|
518
|
+
columnName,
|
|
519
|
+
columnNames,
|
|
520
|
+
await kanbn.currentUser(),
|
|
521
|
+
await kanbn.getContributors()
|
|
522
|
+
)
|
|
512
523
|
.then(answers => {
|
|
513
524
|
taskData.name = answers.name;
|
|
514
525
|
if ('description' in answers) {
|
package/src/controller/board.js
CHANGED
|
@@ -24,16 +24,25 @@ module.exports = async args => {
|
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// Load and hydrate all tracked tasks
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
// Load and hydrate all tracked tasks. A task the board references but which has no task file is
|
|
28
|
+
// skipped rather than fatal, and reported by the hint below
|
|
29
|
+
let tasks, missingTaskFiles;
|
|
30
|
+
try {
|
|
31
|
+
tasks = (await kanbn.loadAllTrackedTasks(index)).map(
|
|
32
|
+
task => kanbn.hydrateTask(index, task)
|
|
33
|
+
);
|
|
34
|
+
missingTaskFiles = await kanbn.findMissingTaskFiles(index);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
utility.error(error);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
31
39
|
|
|
32
40
|
// Show the board
|
|
33
41
|
board
|
|
34
42
|
.show(index, tasks, args.view, args.json)
|
|
35
43
|
.then(async () => {
|
|
36
44
|
const verbose = !args.json && kanbn.isVerbose(index);
|
|
45
|
+
utility.showMissingTaskFilesHint(missingTaskFiles, verbose);
|
|
37
46
|
utility.showDateDriftHint(kanbn.getDateDrift(index, tasks), verbose);
|
|
38
47
|
utility.showBoardCountHint(await workspace.listBoards(), verbose);
|
|
39
48
|
})
|
|
@@ -98,10 +98,13 @@ module.exports = async args => {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// Get assigned
|
|
101
|
+
// Get assigned, expanding "@me" to the current user
|
|
102
102
|
let assigned = null;
|
|
103
103
|
if (args.assigned) {
|
|
104
|
-
assigned = utility.strArg(args.assigned);
|
|
104
|
+
assigned = await utility.expandCurrentUser(kanbn, utility.strArg(args.assigned));
|
|
105
|
+
if (assigned === null) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
// Get columns
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const workspace = require('../main');
|
|
2
2
|
const utility = require('../utility');
|
|
3
3
|
const inquirer = require('inquirer');
|
|
4
|
-
const getGitUsername = require('../git-user-name');
|
|
5
4
|
|
|
6
5
|
// The board this command targets - set from --board before anything else runs
|
|
7
6
|
let kanbn = workspace;
|
|
@@ -46,6 +45,7 @@ function addComment(taskId, text, author) {
|
|
|
46
45
|
.comment(taskId, text, author)
|
|
47
46
|
.then(taskId => {
|
|
48
47
|
console.log(`Added comment to task "${taskId}"`);
|
|
48
|
+
utility.showActionWarnings(kanbn);
|
|
49
49
|
})
|
|
50
50
|
.catch(error => {
|
|
51
51
|
utility.error(error);
|
|
@@ -88,7 +88,10 @@ module.exports = async args => {
|
|
|
88
88
|
if (args.author && typeof args.author === 'string') {
|
|
89
89
|
commentAuthor = utility.strArg(args.author);
|
|
90
90
|
} else {
|
|
91
|
-
|
|
91
|
+
|
|
92
|
+
// Nobody at all is a legitimate answer on a machine with no git identity, and an empty author is
|
|
93
|
+
// what a comment without one looks like
|
|
94
|
+
commentAuthor = (await kanbn.currentUser()) || '';
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
// Add comment interactively
|
|
@@ -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
|
@@ -3,7 +3,6 @@ const utility = require('../utility');
|
|
|
3
3
|
const inquirer = require('inquirer');
|
|
4
4
|
const fuzzy = require('fuzzy');
|
|
5
5
|
const chrono = require('chrono-node');
|
|
6
|
-
const getGitUsername = require('../git-user-name');
|
|
7
6
|
|
|
8
7
|
// The board this command targets - set from --board before anything else runs
|
|
9
8
|
let kanbn = workspace;
|
|
@@ -18,9 +17,11 @@ inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt'))
|
|
|
18
17
|
* @param {string[]} taskIds
|
|
19
18
|
* @param {string} columnName
|
|
20
19
|
* @param {string[]} columnNames
|
|
20
|
+
* @param {?string} currentUser
|
|
21
|
+
* @param {object[]} contributors
|
|
21
22
|
* @return {Promise<any>}
|
|
22
23
|
*/
|
|
23
|
-
async function interactive(taskData, taskIds, columnName, columnNames) {
|
|
24
|
+
async function interactive(taskData, taskIds, columnName, columnNames, currentUser, contributors) {
|
|
24
25
|
const dueDateExists = (
|
|
25
26
|
'metadata' in taskData &&
|
|
26
27
|
'due' in taskData.metadata &&
|
|
@@ -226,13 +227,12 @@ async function interactive(taskData, taskIds, columnName, columnNames) {
|
|
|
226
227
|
default: false,
|
|
227
228
|
when: answers => !assignedExists
|
|
228
229
|
},
|
|
229
|
-
{
|
|
230
|
-
type: 'input',
|
|
230
|
+
utility.assignedPrompt({
|
|
231
231
|
name: 'assigned',
|
|
232
232
|
message: 'Assigned to:',
|
|
233
|
-
default: assignedExists ? taskData.metadata.assigned :
|
|
233
|
+
default: assignedExists ? taskData.metadata.assigned : (currentUser || ''),
|
|
234
234
|
when: answers => answers.setAssigned || answers.editAssigned === 'edit'
|
|
235
|
-
},
|
|
235
|
+
}, contributors),
|
|
236
236
|
{
|
|
237
237
|
type: 'recursive',
|
|
238
238
|
name: 'addSubTasks',
|
|
@@ -450,6 +450,7 @@ function updateTask(taskId, taskData, columnName, unsetFields = []) {
|
|
|
450
450
|
.updateTask(taskId, taskData, columnName, unsetFields)
|
|
451
451
|
.then(taskId => {
|
|
452
452
|
console.log(`Updated task "${taskId}"`);
|
|
453
|
+
utility.showActionWarnings(kanbn);
|
|
453
454
|
})
|
|
454
455
|
.catch(error => {
|
|
455
456
|
utility.error(error);
|
|
@@ -466,18 +467,34 @@ module.exports = async args => {
|
|
|
466
467
|
kanbn = scoped;
|
|
467
468
|
|
|
468
469
|
// Get the task that we're editing
|
|
469
|
-
|
|
470
|
+
let taskId = args._[1];
|
|
470
471
|
if (!taskId) {
|
|
471
472
|
utility.error('No task id specified\nTry running {b}kanbn edit "task id"{b}');
|
|
472
473
|
return;
|
|
473
474
|
}
|
|
474
475
|
|
|
475
|
-
// 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
|
|
476
478
|
try {
|
|
477
479
|
await kanbn.taskExists(taskId);
|
|
478
480
|
} catch (error) {
|
|
479
|
-
|
|
480
|
-
|
|
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
|
+
}
|
|
481
498
|
}
|
|
482
499
|
|
|
483
500
|
// Get the index and make sure it has some columns
|
|
@@ -582,10 +599,12 @@ module.exports = async args => {
|
|
|
582
599
|
if (!('metadata' in taskData)) {
|
|
583
600
|
taskData.metadata = {};
|
|
584
601
|
}
|
|
585
|
-
const gitUsername = getGitUsername();
|
|
586
602
|
if (args.assigned === true) {
|
|
587
|
-
|
|
588
|
-
|
|
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;
|
|
589
608
|
}
|
|
590
609
|
} else {
|
|
591
610
|
taskData.metadata.assigned = utility.strArg(args.assigned);
|
|
@@ -813,7 +832,14 @@ module.exports = async args => {
|
|
|
813
832
|
|
|
814
833
|
// Update task interactively
|
|
815
834
|
if (args.interactive) {
|
|
816
|
-
interactive(
|
|
835
|
+
interactive(
|
|
836
|
+
taskData,
|
|
837
|
+
taskIds,
|
|
838
|
+
columnName,
|
|
839
|
+
columnNames,
|
|
840
|
+
await kanbn.currentUser(),
|
|
841
|
+
await kanbn.getContributors()
|
|
842
|
+
)
|
|
817
843
|
.then(answers => {
|
|
818
844
|
|
|
819
845
|
// Name
|
package/src/controller/find.js
CHANGED
|
@@ -114,6 +114,31 @@ const searchFields = [
|
|
|
114
114
|
name: 'Assigned',
|
|
115
115
|
field: 'assigned',
|
|
116
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'
|
|
117
142
|
}
|
|
118
143
|
];
|
|
119
144
|
|
|
@@ -284,6 +309,24 @@ function convertNumericFilters(filters, filterName) {
|
|
|
284
309
|
return true;
|
|
285
310
|
}
|
|
286
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
|
+
|
|
287
330
|
/**
|
|
288
331
|
* Convert a filter or array of filters to date values
|
|
289
332
|
* @param {object} filters The current filter object
|
|
@@ -343,14 +386,28 @@ module.exports = async args => {
|
|
|
343
386
|
}
|
|
344
387
|
}
|
|
345
388
|
|
|
346
|
-
// 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"
|
|
347
391
|
const filters = {};
|
|
348
|
-
for (let
|
|
349
|
-
if (
|
|
350
|
-
|
|
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];
|
|
351
399
|
}
|
|
352
400
|
}
|
|
353
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;
|
|
407
|
+
}
|
|
408
|
+
filters.assigned = assigned;
|
|
409
|
+
}
|
|
410
|
+
|
|
354
411
|
// Check and convert numeric filters
|
|
355
412
|
if ('count-sub-tasks' in filters) {
|
|
356
413
|
if (!convertNumericFilters(filters, 'count-sub-tasks')) {
|
|
@@ -389,6 +446,20 @@ module.exports = async args => {
|
|
|
389
446
|
}
|
|
390
447
|
}
|
|
391
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
|
+
|
|
392
463
|
// Check date filters
|
|
393
464
|
if ('created' in filters) {
|
|
394
465
|
if (!convertDateFilters(filters, 'created')) {
|
package/src/controller/gantt.js
CHANGED
|
@@ -324,10 +324,13 @@ module.exports = async args => {
|
|
|
324
324
|
kanbn = scoped;
|
|
325
325
|
const index = await kanbn.getIndex();
|
|
326
326
|
|
|
327
|
-
// Get assigned
|
|
327
|
+
// Get assigned, expanding "@me" to the current user
|
|
328
328
|
let assigned = null;
|
|
329
329
|
if (args.assigned) {
|
|
330
|
-
assigned = utility.strArg(args.assigned);
|
|
330
|
+
assigned = await utility.expandCurrentUser(kanbn, utility.strArg(args.assigned));
|
|
331
|
+
if (assigned === null) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
331
334
|
}
|
|
332
335
|
|
|
333
336
|
// Get columns
|
|
@@ -65,10 +65,13 @@ module.exports = async args => {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
// Get assigned
|
|
68
|
+
// Get assigned, expanding "@me" to the current user
|
|
69
69
|
let assigned = null;
|
|
70
70
|
if (args.assigned) {
|
|
71
|
-
assigned = utility.strArg(args.assigned);
|
|
71
|
+
assigned = await utility.expandCurrentUser(kanbn, utility.strArg(args.assigned));
|
|
72
|
+
if (assigned === null) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
// Get task ids
|