@basementuniverse/kanbn 1.1.0 → 2.0.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 +2 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-summary.json +31 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +146 -0
- package/coverage/kanbn/index.html +116 -0
- package/coverage/kanbn/index.js.html +217 -0
- package/coverage/kanbn/src/board.js.html +646 -0
- package/coverage/kanbn/src/controller/add.js.html +1729 -0
- package/coverage/kanbn/src/controller/archive.js.html +277 -0
- package/coverage/kanbn/src/controller/board.js.html +217 -0
- package/coverage/kanbn/src/controller/burndown.js.html +607 -0
- package/coverage/kanbn/src/controller/comment.js.html +397 -0
- package/coverage/kanbn/src/controller/edit.js.html +2920 -0
- package/coverage/kanbn/src/controller/find.js.html +1597 -0
- package/coverage/kanbn/src/controller/gantt.js.html +1342 -0
- package/coverage/kanbn/src/controller/help.js.html +115 -0
- package/coverage/kanbn/src/controller/history.js.html +400 -0
- package/coverage/kanbn/src/controller/index.html +431 -0
- package/coverage/kanbn/src/controller/init.js.html +511 -0
- package/coverage/kanbn/src/controller/move.js.html +514 -0
- package/coverage/kanbn/src/controller/remove-all.js.html +226 -0
- package/coverage/kanbn/src/controller/remove.js.html +307 -0
- package/coverage/kanbn/src/controller/rename.js.html +397 -0
- package/coverage/kanbn/src/controller/restore.js.html +268 -0
- package/coverage/kanbn/src/controller/sort.js.html +1318 -0
- package/coverage/kanbn/src/controller/sprint.js.html +361 -0
- package/coverage/kanbn/src/controller/status.js.html +310 -0
- package/coverage/kanbn/src/controller/task.js.html +244 -0
- package/coverage/kanbn/src/controller/validate.js.html +367 -0
- package/coverage/kanbn/src/controller/version.js.html +100 -0
- package/coverage/kanbn/src/index.html +191 -0
- package/coverage/kanbn/src/main.js.html +9787 -0
- package/coverage/kanbn/src/parse-index.js.html +1063 -0
- package/coverage/kanbn/src/parse-markdown.js.html +202 -0
- package/coverage/kanbn/src/parse-task.js.html +2191 -0
- package/coverage/kanbn/src/utility.js.html +616 -0
- package/coverage/lcov.info +11837 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/tmp/coverage-214292-1787777191526-0.json +1 -0
- package/coverage/tmp/coverage-214293-1787777184569-0.json +1 -0
- package/docs/advanced-configuration.md +2 -0
- package/docs/commands/add.txt +10 -0
- package/docs/commands/board.txt +3 -1
- package/docs/commands/burndown.txt +2 -4
- package/docs/commands/edit.txt +22 -0
- package/docs/commands/find.txt +45 -1
- package/docs/commands/help.txt +1 -0
- package/docs/commands/move.txt +1 -1
- package/docs/commands/sort.txt +13 -2
- package/docs/commands/validate.txt +12 -0
- package/docs/filtering-and-sorting.md +218 -0
- package/docs/index-structure.md +68 -19
- package/docs/index.md +26 -2
- package/docs/migration-2.0.md +92 -0
- package/docs/quick-start.md +37 -1
- package/docs/task-structure.md +12 -3
- package/docs/views.md +229 -0
- package/example/README.md +70 -0
- package/example/advanced/.kanbn/index.md +26 -0
- package/example/advanced/.kanbn/tasks/add-rate-limiting.md +18 -0
- package/example/advanced/.kanbn/tasks/add-sso-support.md +15 -0
- package/example/advanced/.kanbn/tasks/migrate-to-new-cdn.md +14 -0
- package/example/advanced/.kanbn/tasks/refresh-marketing-site.md +14 -0
- package/example/advanced/.kanbn/tasks/rotate-api-keys.md +15 -0
- package/example/advanced/.kanbn/tasks/split-billing-service.md +17 -0
- package/example/advanced/.kanbn/tasks/upgrade-node-runtime.md +19 -0
- package/example/advanced/kanbn.yml +59 -0
- package/example/views/.kanbn/index.md +140 -0
- package/example/views/.kanbn/tasks/add-audit-log-export.md +18 -0
- package/example/views/.kanbn/tasks/add-dark-mode-toggle.md +16 -0
- package/example/views/.kanbn/tasks/build-webhook-retry-queue.md +15 -0
- package/example/views/.kanbn/tasks/fix-avatar-upload-crash.md +13 -0
- package/example/views/.kanbn/tasks/fix-expired-session-redirect.md +15 -0
- package/example/views/.kanbn/tasks/investigate-offline-mode.md +12 -0
- package/example/views/.kanbn/tasks/rewrite-search-indexer.md +12 -0
- package/example/views/.kanbn/tasks/tidy-up-settings-copy.md +12 -0
- package/package.json +4 -3
- package/routes/edit.json +2 -1
- package/routes/validate.json +4 -2
- package/skills/kanbn-plan/references/index-structure.md +1 -0
- package/skills/kanbn-plan/references/task-structure.md +2 -0
- package/src/board.js +1 -0
- package/src/controller/board.js +6 -0
- package/src/controller/edit.js +65 -12
- package/src/controller/gantt.js +46 -1
- package/src/controller/status.js +12 -0
- package/src/controller/task.js +1 -1
- package/src/controller/validate.js +78 -15
- package/src/main.js +324 -152
- package/src/parse-index.js +3 -0
- package/src/parse-markdown.js +3 -1
- package/src/parse-task.js +28 -1
- package/src/utility.js +26 -1
- /package/example/{.kanbn → basic/.kanbn}/index.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/add-basic-activity-feed.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/add-passwordless-login-option.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/add-usage-alert-email-thresholds.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/build-email-template-system.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/build-invoice-download-endpoint.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/build-tenant-settings-page.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/create-organization-switcher.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/create-sandbox-environment-provisioner.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/create-self-serve-cancellation-flow.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/define-product-pricing-strategy.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/design-onboarding-checklist.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/design-team-invite-expiry-flow.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/implement-data-retention-policy-jobs.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/implement-feature-flags-foundation.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/implement-project-creation-wizard.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/implement-stripe-webhook-signature-check.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/implement-team-permissions-ui.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/implement-user-signup-and-login.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/integrate-crm-lead-sync.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/legal-review-terms-and-privacy.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/migrate-legacy-events-to-new-schema.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/optimize-dashboard-first-load.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/prototype-report-export-scheduler.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/publish-internal-qa-checklist.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/setup-ci-pipeline.md +0 -0
package/src/main.js
CHANGED
|
@@ -30,6 +30,12 @@ const DEFAULT_TASK_WORKLOAD_TAGS = {
|
|
|
30
30
|
Huge: 8,
|
|
31
31
|
};
|
|
32
32
|
const DEFAULT_DATE_FORMAT = "d mmm yy, H:MM";
|
|
33
|
+
|
|
34
|
+
// Metadata fields that hold a task's started and completed dates. Boards can override these with the
|
|
35
|
+
// startedField and completedField options, so that a task shared between several boards can carry
|
|
36
|
+
// separate started/completed state for each of them
|
|
37
|
+
const DEFAULT_STARTED_FIELD = "started";
|
|
38
|
+
const DEFAULT_COMPLETED_FIELD = "completed";
|
|
33
39
|
const DEFAULT_TASK_TEMPLATE = "^+^_${overdue ? '^R' : ''}${name}^: ${created ? ('\\n^-^/' + created) : ''}";
|
|
34
40
|
|
|
35
41
|
/**
|
|
@@ -240,25 +246,38 @@ function appendTaskHistory(taskData, historyEvent) {
|
|
|
240
246
|
* @param {object} task
|
|
241
247
|
* @return {boolean} True if the task is in a completed column or has a completed date
|
|
242
248
|
*/
|
|
249
|
+
/**
|
|
250
|
+
* Get the name of the metadata field that holds a task's started date for this board. Boards can
|
|
251
|
+
* point this at a custom date field so that several boards can track their own started/completed
|
|
252
|
+
* state against the same shared task file
|
|
253
|
+
* @param {object} index The index object
|
|
254
|
+
* @return {string} The metadata field name
|
|
255
|
+
*/
|
|
256
|
+
function getStartedField(index) {
|
|
257
|
+
return ("startedField" in index.options && index.options.startedField) || DEFAULT_STARTED_FIELD;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get the name of the metadata field that holds a task's completed date for this board
|
|
262
|
+
* @param {object} index The index object
|
|
263
|
+
* @return {string} The metadata field name
|
|
264
|
+
*/
|
|
265
|
+
function getCompletedField(index) {
|
|
266
|
+
return ("completedField" in index.options && index.options.completedField) || DEFAULT_COMPLETED_FIELD;
|
|
267
|
+
}
|
|
268
|
+
|
|
243
269
|
function taskCompleted(index, task) {
|
|
244
|
-
return (
|
|
245
|
-
"completed" in task.metadata ||
|
|
246
|
-
("completedColumns" in index.options &&
|
|
247
|
-
index.options.completedColumns.indexOf(findTaskColumn(index, task.id)) !== -1)
|
|
248
|
-
);
|
|
270
|
+
return getCompletedField(index) in task.metadata;
|
|
249
271
|
}
|
|
250
272
|
|
|
251
273
|
/**
|
|
252
|
-
*
|
|
274
|
+
* Flatten a task's metadata and computed values into a single object that can be used for sorting
|
|
253
275
|
* @param {object} index The index object
|
|
254
|
-
* @param {object
|
|
255
|
-
* @
|
|
256
|
-
* @param {object[]} sorters A list of sorter objects
|
|
257
|
-
* @return {object} The modified index object
|
|
276
|
+
* @param {object} task The task object
|
|
277
|
+
* @return {object} An object containing all sortable task fields
|
|
258
278
|
*/
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
tasks = tasks.map((task) => ({
|
|
279
|
+
function taskSortFields(index, task) {
|
|
280
|
+
return {
|
|
262
281
|
...task,
|
|
263
282
|
...task.metadata,
|
|
264
283
|
created: "created" in task.metadata ? task.metadata.created : "",
|
|
@@ -279,10 +298,38 @@ function sortColumnInIndex(index, tasks, columnName, sorters) {
|
|
|
279
298
|
comments: task.comments.map((comment) => `${comment.author} ${comment.text}`).join("\n"),
|
|
280
299
|
workload: taskWorkload(index, task),
|
|
281
300
|
progress: taskProgress(index, task),
|
|
282
|
-
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Sort a list of tasks without modifying them, using each task's flattened sort fields
|
|
306
|
+
* @param {object} index The index object
|
|
307
|
+
* @param {object[]} tasks
|
|
308
|
+
* @param {object[]} sorters
|
|
309
|
+
* @return {object[]} The sorted tasks
|
|
310
|
+
*/
|
|
311
|
+
function sortTasksWithFields(index, tasks, sorters) {
|
|
312
|
+
if (!sorters.length) {
|
|
313
|
+
return tasks;
|
|
314
|
+
}
|
|
315
|
+
return sortTasks(
|
|
316
|
+
tasks.map((task) => ({ task, fields: taskSortFields(index, task) })),
|
|
317
|
+
sorters,
|
|
318
|
+
(entry, field) => entry.fields[field]
|
|
319
|
+
).map((entry) => entry.task);
|
|
320
|
+
}
|
|
283
321
|
|
|
284
|
-
|
|
285
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Sort a column in the index
|
|
324
|
+
* @param {object} index The index object
|
|
325
|
+
* @param {object[]} tasks The tasks in the index
|
|
326
|
+
* @param {string} columnName The column to sort
|
|
327
|
+
* @param {object[]} sorters A list of sorter objects
|
|
328
|
+
* @return {object} The modified index object
|
|
329
|
+
*/
|
|
330
|
+
function sortColumnInIndex(index, tasks, columnName, sorters) {
|
|
331
|
+
// Sort the tasks in the target column using their flattened sort fields
|
|
332
|
+
tasks = sortTasksWithFields(index, tasks, sorters);
|
|
286
333
|
|
|
287
334
|
// Save the list of tasks back to the index
|
|
288
335
|
index.columns[columnName] = tasks.map((task) => task.id);
|
|
@@ -293,14 +340,16 @@ function sortColumnInIndex(index, tasks, columnName, sorters) {
|
|
|
293
340
|
* Sort a list of tasks
|
|
294
341
|
* @param {object[]} tasks
|
|
295
342
|
* @param {object[]} sorters
|
|
343
|
+
* @param {?function} [getField=null] An optional accessor used to read a sort field from a task
|
|
296
344
|
* @return {object[]} The sorted tasks
|
|
297
345
|
*/
|
|
298
|
-
function sortTasks(tasks, sorters) {
|
|
346
|
+
function sortTasks(tasks, sorters, getField = null) {
|
|
347
|
+
const readField = getField || ((task, field) => task[field]);
|
|
299
348
|
tasks.sort((a, b) => {
|
|
300
349
|
let compareA, compareB;
|
|
301
350
|
for (let sorter of sorters) {
|
|
302
|
-
compareA = a
|
|
303
|
-
compareB = b
|
|
351
|
+
compareA = readField(a, sorter.field);
|
|
352
|
+
compareB = readField(b, sorter.field);
|
|
304
353
|
if (sorter.filter) {
|
|
305
354
|
compareA = sortFilter(compareA, sorter.filter);
|
|
306
355
|
compareB = sortFilter(compareB, sorter.filter);
|
|
@@ -436,6 +485,22 @@ function filterTasks(index, tasks, filters) {
|
|
|
436
485
|
result = false;
|
|
437
486
|
}
|
|
438
487
|
|
|
488
|
+
// Planned start date
|
|
489
|
+
if (
|
|
490
|
+
"plannedStart" in filters &&
|
|
491
|
+
(!("plannedStart" in task.metadata) || !dateFilter(filters.plannedStart, task.metadata.plannedStart))
|
|
492
|
+
) {
|
|
493
|
+
result = false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Planned finish date
|
|
497
|
+
if (
|
|
498
|
+
"plannedFinish" in filters &&
|
|
499
|
+
(!("plannedFinish" in task.metadata) || !dateFilter(filters.plannedFinish, task.metadata.plannedFinish))
|
|
500
|
+
) {
|
|
501
|
+
result = false;
|
|
502
|
+
}
|
|
503
|
+
|
|
439
504
|
// Workload
|
|
440
505
|
if ("workload" in filters && !numberFilter(filters.workload, taskWorkload(index, task))) {
|
|
441
506
|
result = false;
|
|
@@ -471,12 +536,15 @@ function filterTasks(index, tasks, filters) {
|
|
|
471
536
|
}
|
|
472
537
|
|
|
473
538
|
// Tag
|
|
474
|
-
if ("tag" in filters && !stringFilter(filters.tag, task.metadata.tags.join("\n"))) {
|
|
539
|
+
if ("tag" in filters && !stringFilter(filters.tag, ("tags" in task.metadata ? task.metadata.tags : []).join("\n"))) {
|
|
475
540
|
result = false;
|
|
476
541
|
}
|
|
477
542
|
|
|
478
543
|
// Count tags
|
|
479
|
-
if (
|
|
544
|
+
if (
|
|
545
|
+
"count-tags" in filters &&
|
|
546
|
+
!numberFilter(filters["count-tags"], ("tags" in task.metadata ? task.metadata.tags : []).length)
|
|
547
|
+
) {
|
|
480
548
|
result = false;
|
|
481
549
|
}
|
|
482
550
|
|
|
@@ -656,75 +724,31 @@ function taskWorkloadInPeriod(tasks, metadataProperty, start, end) {
|
|
|
656
724
|
}
|
|
657
725
|
|
|
658
726
|
/**
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
* @param {string|null} columnName
|
|
662
|
-
* @return {boolean}
|
|
663
|
-
*/
|
|
664
|
-
function startedColumn(index, columnName) {
|
|
665
|
-
return (
|
|
666
|
-
!!columnName &&
|
|
667
|
-
"startedColumns" in index.options &&
|
|
668
|
-
index.options.startedColumns.indexOf(columnName) !== -1
|
|
669
|
-
);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Check if a column is configured as a completed column
|
|
674
|
-
* @param {object} index
|
|
675
|
-
* @param {string|null} columnName
|
|
676
|
-
* @return {boolean}
|
|
677
|
-
*/
|
|
678
|
-
function completedColumn(index, columnName) {
|
|
679
|
-
return (
|
|
680
|
-
!!columnName &&
|
|
681
|
-
"completedColumns" in index.options &&
|
|
682
|
-
index.options.completedColumns.indexOf(columnName) !== -1
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Build a task's state at a specific date from history events
|
|
727
|
+
* Find the date at which a task most recently entered one of the specified columns, using its
|
|
728
|
+
* history. Returns null if the task has no history, or none of its events mention those columns
|
|
688
729
|
* @param {object} task
|
|
689
|
-
* @param {
|
|
690
|
-
* @return {?
|
|
730
|
+
* @param {string[]} columnNames
|
|
731
|
+
* @return {?Date}
|
|
691
732
|
*/
|
|
692
|
-
function
|
|
733
|
+
function findColumnEntryDate(task, columnNames) {
|
|
693
734
|
if (!("history" in task) || !Array.isArray(task.history) || task.history.length === 0) {
|
|
694
735
|
return null;
|
|
695
736
|
}
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
column: null,
|
|
701
|
-
};
|
|
702
|
-
const history = [...task.history].sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
703
|
-
history.forEach((historyEvent) => {
|
|
704
|
-
if (!historyEvent.date || historyEvent.date > date) {
|
|
705
|
-
return;
|
|
737
|
+
let result = null;
|
|
738
|
+
for (const historyEvent of [...task.history].sort((a, b) => a.date.getTime() - b.date.getTime())) {
|
|
739
|
+
if (!historyEvent.date) {
|
|
740
|
+
continue;
|
|
706
741
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
state.column = historyEvent.toColumn || state.column;
|
|
715
|
-
break;
|
|
716
|
-
case 'archived':
|
|
717
|
-
state.archived = true;
|
|
718
|
-
break;
|
|
719
|
-
case 'restored':
|
|
720
|
-
state.archived = false;
|
|
721
|
-
state.column = historyEvent.toColumn || state.column;
|
|
722
|
-
break;
|
|
723
|
-
default:
|
|
724
|
-
break;
|
|
742
|
+
const enteredColumn = historyEvent.type === "moved"
|
|
743
|
+
? historyEvent.toColumn
|
|
744
|
+
: (historyEvent.type === "created" || historyEvent.type === "restored")
|
|
745
|
+
? (historyEvent.column || historyEvent.toColumn)
|
|
746
|
+
: null;
|
|
747
|
+
if (enteredColumn && columnNames.indexOf(enteredColumn) !== -1) {
|
|
748
|
+
result = historyEvent.date;
|
|
725
749
|
}
|
|
726
|
-
}
|
|
727
|
-
return
|
|
750
|
+
}
|
|
751
|
+
return result;
|
|
728
752
|
}
|
|
729
753
|
|
|
730
754
|
/**
|
|
@@ -905,14 +929,26 @@ function findDependencyCycle(dependencyMap) {
|
|
|
905
929
|
* @param {Date} now
|
|
906
930
|
* @return {object}
|
|
907
931
|
*/
|
|
908
|
-
function buildGanttSchedule(index, tasks, now) {
|
|
932
|
+
function buildGanttSchedule(index, tasks, now, trackedTaskIds = new Set()) {
|
|
909
933
|
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
910
934
|
const dependencyMap = new Map(tasks.map((task) => [task.id, new Set()]));
|
|
911
935
|
const dependentsMap = new Map();
|
|
912
936
|
const indegree = new Map(tasks.map((task) => [task.id, 0]));
|
|
937
|
+
const externalIncomingDependenciesMap = new Map(tasks.map((task) => [task.id, new Set()]));
|
|
938
|
+
const externalOutgoingDependentsMap = new Map(tasks.map((task) => [task.id, new Set()]));
|
|
939
|
+
|
|
940
|
+
const taskExistsInBoard = (taskId) => trackedTaskIds.has(taskId);
|
|
913
941
|
|
|
914
942
|
const addDependencyEdge = (dependencyId, dependentId) => {
|
|
943
|
+
if (!taskById.has(dependentId)) {
|
|
944
|
+
throw new Error(`Task "${dependentId}" depends on missing task "${dependencyId}"`);
|
|
945
|
+
}
|
|
946
|
+
|
|
915
947
|
if (!taskById.has(dependencyId)) {
|
|
948
|
+
if (taskExistsInBoard(dependencyId)) {
|
|
949
|
+
externalIncomingDependenciesMap.get(dependentId).add(dependencyId);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
916
952
|
throw new Error(`Task "${dependentId}" depends on missing task "${dependencyId}"`);
|
|
917
953
|
}
|
|
918
954
|
|
|
@@ -939,7 +975,13 @@ function buildGanttSchedule(index, tasks, now) {
|
|
|
939
975
|
if (relationType === 'depends-on') {
|
|
940
976
|
addDependencyEdge(relation.task, task.id);
|
|
941
977
|
} else if (relationType === 'blocks') {
|
|
942
|
-
|
|
978
|
+
if (taskById.has(relation.task)) {
|
|
979
|
+
addDependencyEdge(task.id, relation.task);
|
|
980
|
+
} else if (taskExistsInBoard(relation.task)) {
|
|
981
|
+
externalOutgoingDependentsMap.get(task.id).add(relation.task);
|
|
982
|
+
} else {
|
|
983
|
+
throw new Error(`Task "${task.id}" blocks missing task "${relation.task}"`);
|
|
984
|
+
}
|
|
943
985
|
}
|
|
944
986
|
});
|
|
945
987
|
});
|
|
@@ -1036,6 +1078,8 @@ function buildGanttSchedule(index, tasks, now) {
|
|
|
1036
1078
|
const scheduledTask = {
|
|
1037
1079
|
...task,
|
|
1038
1080
|
dependencies,
|
|
1081
|
+
externalIncomingDependencies: [...(externalIncomingDependenciesMap.get(task.id) || [])],
|
|
1082
|
+
externalOutgoingDependents: [...(externalOutgoingDependentsMap.get(task.id) || [])],
|
|
1039
1083
|
start,
|
|
1040
1084
|
end,
|
|
1041
1085
|
blocked: dependencyEnd !== null && preferredStart instanceof Date && dependencyEnd.getTime() > preferredStart.getTime()
|
|
@@ -1057,31 +1101,13 @@ function buildGanttSchedule(index, tasks, now) {
|
|
|
1057
1101
|
};
|
|
1058
1102
|
}
|
|
1059
1103
|
|
|
1060
|
-
/**
|
|
1061
|
-
* Render a gantt bar for a scheduled task
|
|
1062
|
-
* @param {object} task
|
|
1063
|
-
* @param {Date} from
|
|
1064
|
-
* @param {Date} to
|
|
1065
|
-
* @return {string}
|
|
1066
|
-
*/
|
|
1067
|
-
function renderGanttBar(task, from, to) {
|
|
1068
|
-
const result = [];
|
|
1069
|
-
for (let date = new Date(from.getTime()); date <= to; date = new Date(date.getTime() + DAY)) {
|
|
1070
|
-
if (date < task.start || date > task.end) {
|
|
1071
|
-
result.push(' ');
|
|
1072
|
-
} else if (task.completed instanceof Date) {
|
|
1073
|
-
result.push('█');
|
|
1074
|
-
} else if (task.started instanceof Date) {
|
|
1075
|
-
result.push('▓');
|
|
1076
|
-
} else {
|
|
1077
|
-
result.push('░');
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
return result.join('');
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
1104
|
/**
|
|
1084
1105
|
* Get a list of tasks that were started before and/or completed after a date
|
|
1106
|
+
*
|
|
1107
|
+
* A task is active from its started date until its completed date. This used to be derived by
|
|
1108
|
+
* replaying the task's history to work out which column it was in at the time and comparing that
|
|
1109
|
+
* column against the index options, which meant a task's activity depended on column names rather
|
|
1110
|
+
* than on the dates recorded against it
|
|
1085
1111
|
* @param {object} index
|
|
1086
1112
|
* @param {object[]} tasks
|
|
1087
1113
|
* @param {Date} date
|
|
@@ -1089,14 +1115,8 @@ function renderGanttBar(task, from, to) {
|
|
|
1089
1115
|
*/
|
|
1090
1116
|
function getActiveTasksAtDate(index, tasks, date) {
|
|
1091
1117
|
return tasks.filter((task) => (
|
|
1092
|
-
(
|
|
1093
|
-
|
|
1094
|
-
if (historyState !== null) {
|
|
1095
|
-
return historyState.created && !historyState.archived && startedColumn(index, historyState.column) && !completedColumn(index, historyState.column);
|
|
1096
|
-
}
|
|
1097
|
-
return (task.started !== false && task.started <= date) &&
|
|
1098
|
-
(task.completed === false || task.completed > date);
|
|
1099
|
-
})()
|
|
1118
|
+
(task.started !== false && task.started <= date) &&
|
|
1119
|
+
(task.completed === false || task.completed > date)
|
|
1100
1120
|
));
|
|
1101
1121
|
}
|
|
1102
1122
|
|
|
@@ -1190,12 +1210,19 @@ function normaliseDate(date, resolution = 'minutes') {
|
|
|
1190
1210
|
* @param {object} index
|
|
1191
1211
|
* @param {object} taskData
|
|
1192
1212
|
* @param {string} columnName
|
|
1213
|
+
* @param {Date} [date=new Date()] The date to stamp
|
|
1193
1214
|
* @return {object} The updated task data
|
|
1194
1215
|
*/
|
|
1195
|
-
function updateColumnLinkedCustomFields(index, taskData, columnName) {
|
|
1196
|
-
// Update built-in column-linked metadata properties first (started and completed dates)
|
|
1197
|
-
|
|
1198
|
-
|
|
1216
|
+
function updateColumnLinkedCustomFields(index, taskData, columnName, date = new Date()) {
|
|
1217
|
+
// Update built-in column-linked metadata properties first (started and completed dates). The
|
|
1218
|
+
// columns are always listed in startedColumns/completedColumns, but the field that gets written
|
|
1219
|
+
// can be pointed elsewhere with startedField/completedField
|
|
1220
|
+
taskData = updateColumnLinkedCustomField(
|
|
1221
|
+
index, taskData, columnName, getCompletedField(index), "once", "completedColumns", date
|
|
1222
|
+
);
|
|
1223
|
+
taskData = updateColumnLinkedCustomField(
|
|
1224
|
+
index, taskData, columnName, getStartedField(index), "once", "startedColumns", date
|
|
1225
|
+
);
|
|
1199
1226
|
|
|
1200
1227
|
// Update column-linked custom fields
|
|
1201
1228
|
if ("customFields" in index.options) {
|
|
@@ -1206,7 +1233,9 @@ function updateColumnLinkedCustomFields(index, taskData, columnName) {
|
|
|
1206
1233
|
taskData,
|
|
1207
1234
|
columnName,
|
|
1208
1235
|
customField.name,
|
|
1209
|
-
customField.updateDate || "none"
|
|
1236
|
+
customField.updateDate || "none",
|
|
1237
|
+
null,
|
|
1238
|
+
date
|
|
1210
1239
|
);
|
|
1211
1240
|
}
|
|
1212
1241
|
}
|
|
@@ -1225,17 +1254,23 @@ function updateColumnLinkedCustomFields(index, taskData, columnName) {
|
|
|
1225
1254
|
* @param {string} columnName
|
|
1226
1255
|
* @param {string} fieldName
|
|
1227
1256
|
* @param {string} [updateCriteria='none']
|
|
1257
|
+
* @param {?string} [columnListName=null] The option holding the linked column names, or null to use
|
|
1258
|
+
* the `${fieldName}Columns` convention
|
|
1259
|
+
* @param {Date} [date=new Date()] The date to stamp
|
|
1228
1260
|
*/
|
|
1229
|
-
function updateColumnLinkedCustomField(
|
|
1230
|
-
|
|
1261
|
+
function updateColumnLinkedCustomField(
|
|
1262
|
+
index, taskData, columnName, fieldName, updateCriteria = "none", columnListName = null,
|
|
1263
|
+
date = new Date()
|
|
1264
|
+
) {
|
|
1265
|
+
const columnList = columnListName || `${fieldName}Columns`;
|
|
1231
1266
|
if (columnList in index.options && index.options[columnList].indexOf(columnName) !== -1) {
|
|
1232
1267
|
switch (updateCriteria) {
|
|
1233
1268
|
case "always":
|
|
1234
|
-
taskData = setTaskMetadata(taskData, fieldName,
|
|
1269
|
+
taskData = setTaskMetadata(taskData, fieldName, date);
|
|
1235
1270
|
break;
|
|
1236
1271
|
case "once":
|
|
1237
1272
|
if (!(fieldName in taskData.metadata && taskData.metadata[fieldName])) {
|
|
1238
|
-
taskData = setTaskMetadata(taskData, fieldName,
|
|
1273
|
+
taskData = setTaskMetadata(taskData, fieldName, date);
|
|
1239
1274
|
}
|
|
1240
1275
|
break;
|
|
1241
1276
|
default:
|
|
@@ -1411,7 +1446,7 @@ class Kanbn {
|
|
|
1411
1446
|
* @return {Promise<task>} The task
|
|
1412
1447
|
*/
|
|
1413
1448
|
async getTask(taskId) {
|
|
1414
|
-
this.taskExists(taskId);
|
|
1449
|
+
await this.taskExists(taskId);
|
|
1415
1450
|
return this.loadTask(taskId);
|
|
1416
1451
|
}
|
|
1417
1452
|
|
|
@@ -1436,7 +1471,8 @@ class Kanbn {
|
|
|
1436
1471
|
|
|
1437
1472
|
// A task is overdue if it's due date is in the past and the task is not in a completed column
|
|
1438
1473
|
// or doesn't have a completed dates
|
|
1439
|
-
const
|
|
1474
|
+
const completedField = getCompletedField(index);
|
|
1475
|
+
const completedDate = completedField in task.metadata ? task.metadata[completedField] : null;
|
|
1440
1476
|
|
|
1441
1477
|
// Get task due delta - this is the difference between now and the due date, or if the task is completed
|
|
1442
1478
|
// this is the difference between the completed and due dates
|
|
@@ -1478,7 +1514,7 @@ class Kanbn {
|
|
|
1478
1514
|
* @return {object[]} A filtered and sorted list of tasks
|
|
1479
1515
|
*/
|
|
1480
1516
|
filterAndSortTasks(index, tasks, filters, sorters) {
|
|
1481
|
-
return
|
|
1517
|
+
return sortTasksWithFields(index, filterTasks(index, tasks, filters), sorters);
|
|
1482
1518
|
}
|
|
1483
1519
|
|
|
1484
1520
|
/**
|
|
@@ -1857,9 +1893,10 @@ class Kanbn {
|
|
|
1857
1893
|
* @param {string} taskId The id of the task to update
|
|
1858
1894
|
* @param {object} taskData The new task data
|
|
1859
1895
|
* @param {?string} [columnName=null] The column name to move this task to, or null if not moving this task
|
|
1896
|
+
* @param {string[]} [unsetFields=[]] Metadata field names to remove from the task
|
|
1860
1897
|
* @return {Promise<string>} The id of the task that was updated
|
|
1861
1898
|
*/
|
|
1862
|
-
async updateTask(taskId, taskData, columnName = null) {
|
|
1899
|
+
async updateTask(taskId, taskData, columnName = null, unsetFields = []) {
|
|
1863
1900
|
// Check if this folder has been initialised
|
|
1864
1901
|
if (!(await this.initialised())) {
|
|
1865
1902
|
throw new Error("Not initialised in this folder");
|
|
@@ -1921,6 +1958,19 @@ class Kanbn {
|
|
|
1921
1958
|
} else {
|
|
1922
1959
|
await this.saveIndex(index);
|
|
1923
1960
|
}
|
|
1961
|
+
|
|
1962
|
+
// Remove any explicitly unset metadata fields last. This has to happen after the move, because
|
|
1963
|
+
// moving into a started or completed column stamps the linked date fields - an explicit unset
|
|
1964
|
+
// from the user should win over that automatic stamp
|
|
1965
|
+
if (unsetFields.length) {
|
|
1966
|
+
const finalTaskData = await this.loadTask(taskId);
|
|
1967
|
+
if ("metadata" in finalTaskData) {
|
|
1968
|
+
for (const field of unsetFields) {
|
|
1969
|
+
delete finalTaskData.metadata[field];
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
await this.saveTask(getTaskPath(await this.getTaskFolderPath(), taskId), finalTaskData);
|
|
1973
|
+
}
|
|
1924
1974
|
return taskId;
|
|
1925
1975
|
}
|
|
1926
1976
|
|
|
@@ -2009,13 +2059,18 @@ class Kanbn {
|
|
|
2009
2059
|
// Find current column before mutating data
|
|
2010
2060
|
const currentColumnName = findTaskColumn(index, taskId);
|
|
2011
2061
|
|
|
2062
|
+
// Stamp every date this move writes with the same timestamp, so that the history event and the
|
|
2063
|
+
// dates it backfills from can't disagree by a millisecond
|
|
2064
|
+
const moveDate = new Date();
|
|
2065
|
+
|
|
2012
2066
|
// Update the task's updated date
|
|
2013
2067
|
let taskData = await this.loadTask(taskId);
|
|
2014
|
-
taskData = setTaskMetadata(taskData, "updated",
|
|
2068
|
+
taskData = setTaskMetadata(taskData, "updated", moveDate);
|
|
2015
2069
|
|
|
2016
2070
|
// Add history only when a task changes columns
|
|
2017
2071
|
if (currentColumnName !== columnName) {
|
|
2018
2072
|
taskData = appendTaskHistory(taskData, {
|
|
2073
|
+
date: moveDate,
|
|
2019
2074
|
type: 'moved',
|
|
2020
2075
|
fromColumn: currentColumnName,
|
|
2021
2076
|
toColumn: columnName
|
|
@@ -2023,7 +2078,7 @@ class Kanbn {
|
|
|
2023
2078
|
}
|
|
2024
2079
|
|
|
2025
2080
|
// Update task metadata dates
|
|
2026
|
-
taskData = updateColumnLinkedCustomFields(index, taskData, columnName);
|
|
2081
|
+
taskData = updateColumnLinkedCustomFields(index, taskData, columnName, moveDate);
|
|
2027
2082
|
await this.saveTask(getTaskPath(await this.getTaskFolderPath(), taskId), taskData);
|
|
2028
2083
|
|
|
2029
2084
|
// If we're moving the task to a new position, calculate the absolute position
|
|
@@ -2112,6 +2167,8 @@ class Kanbn {
|
|
|
2112
2167
|
// Get index and column names
|
|
2113
2168
|
const index = await this.loadIndex();
|
|
2114
2169
|
const columnNames = Object.keys(index.columns);
|
|
2170
|
+
const startedField = getStartedField(index);
|
|
2171
|
+
const completedField = getCompletedField(index);
|
|
2115
2172
|
|
|
2116
2173
|
// Prepare output
|
|
2117
2174
|
const result = {
|
|
@@ -2276,8 +2333,8 @@ class Kanbn {
|
|
|
2276
2333
|
|
|
2277
2334
|
// Add task workload information for the sprint
|
|
2278
2335
|
result.sprint.created = taskWorkloadInPeriod(tasks, "created", sprintStartDate, sprintEndDate);
|
|
2279
|
-
result.sprint.started = taskWorkloadInPeriod(tasks,
|
|
2280
|
-
result.sprint.completed = taskWorkloadInPeriod(tasks,
|
|
2336
|
+
result.sprint.started = taskWorkloadInPeriod(tasks, startedField, sprintStartDate, sprintEndDate);
|
|
2337
|
+
result.sprint.completed = taskWorkloadInPeriod(tasks, completedField, sprintStartDate, sprintEndDate);
|
|
2281
2338
|
result.sprint.due = taskWorkloadInPeriod(tasks, "due", sprintStartDate, sprintEndDate);
|
|
2282
2339
|
|
|
2283
2340
|
// Add custom date property workload information for the sprint
|
|
@@ -2311,8 +2368,8 @@ class Kanbn {
|
|
|
2311
2368
|
result.period.end = periodEnd = new Date(Math.max(...dates));
|
|
2312
2369
|
}
|
|
2313
2370
|
result.period.created = taskWorkloadInPeriod(tasks, "created", periodStart, periodEnd);
|
|
2314
|
-
result.period.started = taskWorkloadInPeriod(tasks,
|
|
2315
|
-
result.period.completed = taskWorkloadInPeriod(tasks,
|
|
2371
|
+
result.period.started = taskWorkloadInPeriod(tasks, startedField, periodStart, periodEnd);
|
|
2372
|
+
result.period.completed = taskWorkloadInPeriod(tasks, completedField, periodStart, periodEnd);
|
|
2316
2373
|
result.period.due = taskWorkloadInPeriod(tasks, "due", periodStart, periodEnd);
|
|
2317
2374
|
|
|
2318
2375
|
// Add custom date property workload information for the selected date range
|
|
@@ -2386,6 +2443,122 @@ class Kanbn {
|
|
|
2386
2443
|
return true;
|
|
2387
2444
|
}
|
|
2388
2445
|
|
|
2446
|
+
/**
|
|
2447
|
+
* Find tasks whose started/completed dates disagree with the column they're in
|
|
2448
|
+
*
|
|
2449
|
+
* Task state comes from metadata dates only, so a task moved between columns by hand (by editing
|
|
2450
|
+
* the index file, or through a merge) won't have had its dates updated. This reports both
|
|
2451
|
+
* directions: a task sitting in a started or completed column with no date, and a task carrying a
|
|
2452
|
+
* completed date while sitting somewhere else
|
|
2453
|
+
* @return {Promise<object[]>} A list of drift entries
|
|
2454
|
+
*/
|
|
2455
|
+
async findDateDrift() {
|
|
2456
|
+
// Check if this folder has been initialised
|
|
2457
|
+
if (!(await this.initialised())) {
|
|
2458
|
+
throw new Error("Not initialised in this folder");
|
|
2459
|
+
}
|
|
2460
|
+
const index = await this.loadIndex();
|
|
2461
|
+
const tasks = [];
|
|
2462
|
+
for (const taskId of getTrackedTaskIds(index)) {
|
|
2463
|
+
try {
|
|
2464
|
+
tasks.push(await this.loadTask(taskId));
|
|
2465
|
+
} catch (error) {
|
|
2466
|
+
// Unreadable tasks are reported by validate(), not here
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
return this.getDateDrift(index, tasks);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/**
|
|
2473
|
+
* Find date drift in a list of tasks that have already been loaded
|
|
2474
|
+
* @param {object} index The index object
|
|
2475
|
+
* @param {object[]} tasks Loaded task objects
|
|
2476
|
+
* @return {object[]} A list of drift entries
|
|
2477
|
+
*/
|
|
2478
|
+
getDateDrift(index, tasks) {
|
|
2479
|
+
const startedField = getStartedField(index);
|
|
2480
|
+
const completedField = getCompletedField(index);
|
|
2481
|
+
const startedColumns = "startedColumns" in index.options ? index.options.startedColumns : [];
|
|
2482
|
+
const completedColumns = "completedColumns" in index.options ? index.options.completedColumns : [];
|
|
2483
|
+
const drift = [];
|
|
2484
|
+
|
|
2485
|
+
for (const task of tasks) {
|
|
2486
|
+
const column = findTaskColumn(index, task.id);
|
|
2487
|
+
|
|
2488
|
+
// A task in a started or completed column with no date to say when that happened. The date is
|
|
2489
|
+
// usually recoverable from history, so these can be backfilled
|
|
2490
|
+
for (const [field, columns] of [[completedField, completedColumns], [startedField, startedColumns]]) {
|
|
2491
|
+
if (columns.indexOf(column) === -1 || field in task.metadata) {
|
|
2492
|
+
continue;
|
|
2493
|
+
}
|
|
2494
|
+
const historyDate = findColumnEntryDate(task, columns);
|
|
2495
|
+
const date = historyDate
|
|
2496
|
+
|| ("updated" in task.metadata ? task.metadata.updated : null)
|
|
2497
|
+
|| ("created" in task.metadata ? task.metadata.created : null);
|
|
2498
|
+
drift.push({
|
|
2499
|
+
task: task.id,
|
|
2500
|
+
column,
|
|
2501
|
+
field,
|
|
2502
|
+
issue: "missing",
|
|
2503
|
+
message: `task is in column "${column}" but has no ${field} date`,
|
|
2504
|
+
fixable: date !== null,
|
|
2505
|
+
date,
|
|
2506
|
+
source: date === null
|
|
2507
|
+
? null
|
|
2508
|
+
: historyDate
|
|
2509
|
+
? "history"
|
|
2510
|
+
: "updated" in task.metadata && task.metadata.updated === date
|
|
2511
|
+
? "updated"
|
|
2512
|
+
: "created"
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// A task carrying a completed date while sitting outside the completed columns. This is left
|
|
2517
|
+
// alone: the date records something that really happened, and removing it would lose it
|
|
2518
|
+
if (
|
|
2519
|
+
completedColumns.length > 0 &&
|
|
2520
|
+
completedField in task.metadata &&
|
|
2521
|
+
completedColumns.indexOf(column) === -1
|
|
2522
|
+
) {
|
|
2523
|
+
drift.push({
|
|
2524
|
+
task: task.id,
|
|
2525
|
+
column,
|
|
2526
|
+
field: completedField,
|
|
2527
|
+
issue: "unexpected",
|
|
2528
|
+
message: `task has a ${completedField} date but is in column "${column}"`,
|
|
2529
|
+
fixable: false,
|
|
2530
|
+
date: null,
|
|
2531
|
+
source: null
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
return drift;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
/**
|
|
2539
|
+
* Check whether hints and notices should be shown for this board
|
|
2540
|
+
* @param {object} index The index object
|
|
2541
|
+
* @return {boolean} True if hints should be shown
|
|
2542
|
+
*/
|
|
2543
|
+
isVerbose(index) {
|
|
2544
|
+
return !("verbose" in index.options) || index.options.verbose !== false;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
/**
|
|
2548
|
+
* Backfill missing started/completed dates for tasks that are in a started or completed column
|
|
2549
|
+
* @return {Promise<object[]>} The drift entries that were fixed
|
|
2550
|
+
*/
|
|
2551
|
+
async fixDateDrift() {
|
|
2552
|
+
const fixed = [];
|
|
2553
|
+
for (const item of (await this.findDateDrift()).filter((d) => d.fixable && d.issue === "missing")) {
|
|
2554
|
+
const task = await this.loadTask(item.task);
|
|
2555
|
+
setTaskMetadata(task, item.field, item.date);
|
|
2556
|
+
await this.saveTask(getTaskPath(await this.getTaskFolderPath(), item.task), task);
|
|
2557
|
+
fixed.push(item);
|
|
2558
|
+
}
|
|
2559
|
+
return fixed;
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2389
2562
|
/**
|
|
2390
2563
|
* Sort a column in the index
|
|
2391
2564
|
* @param {string} columnName The column name to sort
|
|
@@ -2481,6 +2654,8 @@ class Kanbn {
|
|
|
2481
2654
|
|
|
2482
2655
|
// Get index and tasks
|
|
2483
2656
|
const index = await this.loadIndex();
|
|
2657
|
+
const startedField = getStartedField(index);
|
|
2658
|
+
const completedField = getCompletedField(index);
|
|
2484
2659
|
const tasks = [...(await this.loadAllTrackedTasks(index))]
|
|
2485
2660
|
.map((task) => {
|
|
2486
2661
|
const taskColumn = findTaskColumn(index, task.id);
|
|
@@ -2488,18 +2663,12 @@ class Kanbn {
|
|
|
2488
2663
|
return {
|
|
2489
2664
|
...task,
|
|
2490
2665
|
created,
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
completed:
|
|
2498
|
-
"completed" in task.metadata
|
|
2499
|
-
? task.metadata.completed
|
|
2500
|
-
: "completedColumns" in index.options && index.options.completedColumns.indexOf(taskColumn) !== -1
|
|
2501
|
-
? created
|
|
2502
|
-
: false,
|
|
2666
|
+
|
|
2667
|
+
// A task's started and completed dates come from its metadata only. They used to fall back
|
|
2668
|
+
// to the task's created date whenever it sat in a started or completed column, which
|
|
2669
|
+
// invented timings that had nothing to do with when the work actually happened
|
|
2670
|
+
started: startedField in task.metadata ? task.metadata[startedField] : false,
|
|
2671
|
+
completed: completedField in task.metadata ? task.metadata[completedField] : false,
|
|
2503
2672
|
progress: taskProgress(index, task),
|
|
2504
2673
|
assigned: "assigned" in task.metadata ? task.metadata.assigned : null,
|
|
2505
2674
|
workload: taskWorkload(index, task),
|
|
@@ -2533,8 +2702,8 @@ class Kanbn {
|
|
|
2533
2702
|
.map((t) =>
|
|
2534
2703
|
[
|
|
2535
2704
|
"created" in t.metadata && t.metadata.created,
|
|
2536
|
-
|
|
2537
|
-
|
|
2705
|
+
startedField in t.metadata && t.metadata[startedField],
|
|
2706
|
+
completedField in t.metadata && (t.metadata[completedField] || new Date(8640000000000000))
|
|
2538
2707
|
].filter((d) => d)
|
|
2539
2708
|
)
|
|
2540
2709
|
.flat()
|
|
@@ -2811,6 +2980,8 @@ class Kanbn {
|
|
|
2811
2980
|
|
|
2812
2981
|
// Get index and tasks
|
|
2813
2982
|
const index = await this.loadIndex();
|
|
2983
|
+
const startedField = getStartedField(index);
|
|
2984
|
+
const completedField = getCompletedField(index);
|
|
2814
2985
|
const tasks = [...(await this.loadAllTrackedTasks(index))]
|
|
2815
2986
|
.map((task) => {
|
|
2816
2987
|
const taskColumn = findTaskColumn(index, task.id);
|
|
@@ -2818,8 +2989,8 @@ class Kanbn {
|
|
|
2818
2989
|
return {
|
|
2819
2990
|
...task,
|
|
2820
2991
|
created,
|
|
2821
|
-
started:
|
|
2822
|
-
completed:
|
|
2992
|
+
started: startedField in task.metadata ? task.metadata[startedField] : false,
|
|
2993
|
+
completed: completedField in task.metadata ? task.metadata[completedField] : false,
|
|
2823
2994
|
plannedStart: "plannedStart" in task.metadata ? task.metadata.plannedStart : false,
|
|
2824
2995
|
plannedFinish: "plannedFinish" in task.metadata ? task.metadata.plannedFinish : false,
|
|
2825
2996
|
workload: taskWorkload(index, task),
|
|
@@ -2850,7 +3021,8 @@ class Kanbn {
|
|
|
2850
3021
|
}
|
|
2851
3022
|
);
|
|
2852
3023
|
|
|
2853
|
-
|
|
3024
|
+
const trackedTaskIds = getTrackedTaskIds(index);
|
|
3025
|
+
return buildGanttSchedule(index, tasks, effectiveNow, trackedTaskIds);
|
|
2854
3026
|
}
|
|
2855
3027
|
|
|
2856
3028
|
/**
|