@harborppm/cli 0.85.0 → 0.87.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/dist/app.mjs +261 -62
- package/lib/cli.mjs +31 -5
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -1898,7 +1898,20 @@ function boardPositions(tasks) {
|
|
|
1898
1898
|
return out;
|
|
1899
1899
|
});
|
|
1900
1900
|
}
|
|
1901
|
-
|
|
1901
|
+
/** For each task id, the tasks that name it as their parent, in list order (src/lib/subtasks.ts reads it). */
|
|
1902
|
+
function childrenByParent(tasks) {
|
|
1903
|
+
return cached$1(children, tasks, () => {
|
|
1904
|
+
const out = /* @__PURE__ */ new Map();
|
|
1905
|
+
for (const t of tasks) {
|
|
1906
|
+
if (!t.parentTaskId) continue;
|
|
1907
|
+
const list = out.get(t.parentTaskId);
|
|
1908
|
+
if (list) list.push(t);
|
|
1909
|
+
else out.set(t.parentTaskId, [t]);
|
|
1910
|
+
}
|
|
1911
|
+
return out;
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
var ids$1, projectTasks$1, EMPTY, waiting, positions, children;
|
|
1902
1915
|
var init_indexes = __esmMin((() => {
|
|
1903
1916
|
init_blockers();
|
|
1904
1917
|
ids$1 = /* @__PURE__ */ new WeakMap();
|
|
@@ -1906,6 +1919,7 @@ var init_indexes = __esmMin((() => {
|
|
|
1906
1919
|
EMPTY = Object.freeze([]);
|
|
1907
1920
|
waiting = /* @__PURE__ */ new WeakMap();
|
|
1908
1921
|
positions = /* @__PURE__ */ new WeakMap();
|
|
1922
|
+
children = /* @__PURE__ */ new WeakMap();
|
|
1909
1923
|
}));
|
|
1910
1924
|
|
|
1911
1925
|
// src/lib/premortem.ts
|
|
@@ -4765,6 +4779,54 @@ var init_task_people = __esmMin((() => {
|
|
|
4765
4779
|
init_agents();
|
|
4766
4780
|
}));
|
|
4767
4781
|
|
|
4782
|
+
// src/lib/subtasks.ts
|
|
4783
|
+
/** The parent `task` is a piece of, when that link holds; otherwise null. */
|
|
4784
|
+
function parentOf(task, tasks) {
|
|
4785
|
+
const id = task.parentTaskId;
|
|
4786
|
+
if (!id || id === task.id) return null;
|
|
4787
|
+
const parent = byId(tasks).get(id);
|
|
4788
|
+
if (!parent || parent.projectId !== task.projectId || parent.parentTaskId) return null;
|
|
4789
|
+
return parent;
|
|
4790
|
+
}
|
|
4791
|
+
/** Whether `task` shows inside its parent rather than on its own. */
|
|
4792
|
+
function isSubtask(task, tasks) {
|
|
4793
|
+
return parentOf(task, tasks) !== null;
|
|
4794
|
+
}
|
|
4795
|
+
/** The subtasks of `parent`, in list order. Empty for a task that is itself a subtask. */
|
|
4796
|
+
function subtasksOf(parent, tasks) {
|
|
4797
|
+
if (isSubtask(parent, tasks)) return NONE$2;
|
|
4798
|
+
const list = childrenByParent(tasks).get(parent.id);
|
|
4799
|
+
if (!list) return NONE$2;
|
|
4800
|
+
return list.filter((t) => t.projectId === parent.projectId && t.id !== parent.id);
|
|
4801
|
+
}
|
|
4802
|
+
/** The tasks that stand on their own: what a board or a list shows. */
|
|
4803
|
+
function topLevel(list, tasks) {
|
|
4804
|
+
return list.filter((t) => !isSubtask(t, tasks));
|
|
4805
|
+
}
|
|
4806
|
+
/**
|
|
4807
|
+
* Why `childId` can't become a subtask of `parentId`, or null when it can.
|
|
4808
|
+
* The parent must be another task in the same project that isn't a subtask,
|
|
4809
|
+
* and the child mustn't have subtasks of its own (one level only).
|
|
4810
|
+
*/
|
|
4811
|
+
function subtaskProblem(childId, parentId, tasks) {
|
|
4812
|
+
const index = byId(tasks);
|
|
4813
|
+
const child = index.get(childId);
|
|
4814
|
+
const parent = index.get(parentId);
|
|
4815
|
+
if (!parent) return "That task isn't here any more.";
|
|
4816
|
+
if (childId === parentId) return "A task can't be a subtask of itself.";
|
|
4817
|
+
if (child && child.projectId !== parent.projectId) return "A subtask has to be in the same project as its parent.";
|
|
4818
|
+
if (isSubtask(parent, tasks)) return "That task is already a subtask; subtasks go one level deep.";
|
|
4819
|
+
if (child && subtasksOf(child, tasks).length > 0) return "This task has subtasks of its own, so it can't go inside another.";
|
|
4820
|
+
if (subtasksOf(parent, tasks).filter((t) => t.id !== childId).length >= 50) return `A task can hold up to ${50} subtasks.`;
|
|
4821
|
+
return null;
|
|
4822
|
+
}
|
|
4823
|
+
var SUBTASKS_MAX, NONE$2;
|
|
4824
|
+
var init_subtasks = __esmMin((() => {
|
|
4825
|
+
init_indexes();
|
|
4826
|
+
SUBTASKS_MAX = 50;
|
|
4827
|
+
NONE$2 = Object.freeze([]);
|
|
4828
|
+
}));
|
|
4829
|
+
|
|
4768
4830
|
// src/lib/board.ts
|
|
4769
4831
|
/** The column's cap after "No cap" (`uncapped`) or a number is picked. */
|
|
4770
4832
|
function withWipLimit(col, limit) {
|
|
@@ -4943,11 +5005,12 @@ function wipFullMessage(overflow) {
|
|
|
4943
5005
|
function columnWip(data, projectId, status, extra = 1) {
|
|
4944
5006
|
const col = boardConfigOf(data, projectId).columns.find((c) => c.status === status);
|
|
4945
5007
|
if (!col) return null;
|
|
4946
|
-
const current = tasksOf(data.tasks, projectId).filter((t) => t.status === status).length;
|
|
5008
|
+
const current = tasksOf(data.tasks, projectId).filter((t) => t.status === status && !isSubtask(t, data.tasks)).length;
|
|
4947
5009
|
return wipOverflow(col, current, extra);
|
|
4948
5010
|
}
|
|
4949
5011
|
var BOARD_PRESETS, ALL_FIELDS, DEFAULT_DOING_WIP, DEFAULT_REVIEW_WIP, WIP_CAP_CHOICES;
|
|
4950
5012
|
var init_board = __esmMin((() => {
|
|
5013
|
+
init_subtasks();
|
|
4951
5014
|
init_indexes();
|
|
4952
5015
|
init_swatches();
|
|
4953
5016
|
BOARD_PRESETS = [
|
|
@@ -5266,6 +5329,49 @@ function cascadeSummary(counts, kind) {
|
|
|
5266
5329
|
if (list.length === 1) return list[0];
|
|
5267
5330
|
return `${list.slice(0, -1).join(", ")} and ${list.at(-1)}`;
|
|
5268
5331
|
}
|
|
5332
|
+
/** `data` with the cut links put back, wherever both ends exist again. */
|
|
5333
|
+
function relink(data, links) {
|
|
5334
|
+
const out = {};
|
|
5335
|
+
const exists = (key) => new Set(data[key].map((r) => r.id));
|
|
5336
|
+
const targetOf = {
|
|
5337
|
+
programs: "programs",
|
|
5338
|
+
projects: "projects",
|
|
5339
|
+
tasks: "tasks",
|
|
5340
|
+
milestones: "projects"
|
|
5341
|
+
};
|
|
5342
|
+
for (const key of [
|
|
5343
|
+
"programs",
|
|
5344
|
+
"projects",
|
|
5345
|
+
"tasks",
|
|
5346
|
+
"milestones"
|
|
5347
|
+
]) {
|
|
5348
|
+
const cut = links.blockers[key];
|
|
5349
|
+
if (!cut) continue;
|
|
5350
|
+
const there = exists(targetOf[key]);
|
|
5351
|
+
out[key] = data[key].map((row) => {
|
|
5352
|
+
const was = cut[row.id];
|
|
5353
|
+
if (!was) return row;
|
|
5354
|
+
const ids = [.../* @__PURE__ */ new Set([...was, ...blockerIds(row)])].filter((id) => there.has(id));
|
|
5355
|
+
return ids.length === blockerIds(row).length ? row : {
|
|
5356
|
+
...row,
|
|
5357
|
+
...blockersPatch(ids)
|
|
5358
|
+
};
|
|
5359
|
+
});
|
|
5360
|
+
}
|
|
5361
|
+
if (Object.keys(links.focus).length) {
|
|
5362
|
+
const tasks = exists("tasks");
|
|
5363
|
+
out.ropes = (data.ropes ?? []).map((r) => {
|
|
5364
|
+
const lost = links.focus[`${r.personId}|${r.date}`];
|
|
5365
|
+
if (!lost) return r;
|
|
5366
|
+
const back = lost.filter((id) => tasks.has(id) && !r.taskIds.includes(id));
|
|
5367
|
+
return back.length ? {
|
|
5368
|
+
...r,
|
|
5369
|
+
taskIds: [...r.taskIds, ...back]
|
|
5370
|
+
} : r;
|
|
5371
|
+
});
|
|
5372
|
+
}
|
|
5373
|
+
return out;
|
|
5374
|
+
}
|
|
5269
5375
|
var init_cascade = __esmMin((() => {
|
|
5270
5376
|
init_blockers();
|
|
5271
5377
|
}));
|
|
@@ -5731,15 +5837,19 @@ function roadmapDoneCount(data, portfolioId) {
|
|
|
5731
5837
|
}
|
|
5732
5838
|
|
|
5733
5839
|
// src/lib/search.ts
|
|
5840
|
+
/** Lower case without accents, so "cafe" finds "Café" and "Zoë" finds "Zoe". */
|
|
5841
|
+
function fold(text) {
|
|
5842
|
+
return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
5843
|
+
}
|
|
5734
5844
|
function tokensOf(query) {
|
|
5735
|
-
return query
|
|
5845
|
+
return fold(query).split(/\s+/).filter(Boolean);
|
|
5736
5846
|
}
|
|
5737
5847
|
/** Score a thing by its own name first, then by everything else it carries. */
|
|
5738
5848
|
function scoreMatch(query, name, ...secondary) {
|
|
5739
|
-
const q = query.trim()
|
|
5849
|
+
const q = fold(query.trim());
|
|
5740
5850
|
if (!q) return SCORE.anywhere;
|
|
5741
5851
|
const tokens = tokensOf(q);
|
|
5742
|
-
const n = name
|
|
5852
|
+
const n = fold(name);
|
|
5743
5853
|
if (n === q) return SCORE.exact;
|
|
5744
5854
|
if (n.startsWith(q)) return SCORE.prefix;
|
|
5745
5855
|
const words = n.split(/\s+/);
|
|
@@ -5747,7 +5857,7 @@ function scoreMatch(query, name, ...secondary) {
|
|
|
5747
5857
|
const bonus = tokens.every((t) => words.some((w) => w.startsWith(t))) ? 5 : 0;
|
|
5748
5858
|
return SCORE.name + bonus;
|
|
5749
5859
|
}
|
|
5750
|
-
const blob = [n, ...secondary.filter((s) => Boolean(s)).map(
|
|
5860
|
+
const blob = [n, ...secondary.filter((s) => Boolean(s)).map(fold)].join(" ");
|
|
5751
5861
|
return tokens.every((t) => blob.includes(t)) ? SCORE.anywhere : 0;
|
|
5752
5862
|
}
|
|
5753
5863
|
/** Keep what matches, best first. Ties keep the caller's order. */
|
|
@@ -5932,6 +6042,7 @@ init_date_fns();
|
|
|
5932
6042
|
init_date_text();
|
|
5933
6043
|
init_first_voyage();
|
|
5934
6044
|
init_indexes();
|
|
6045
|
+
init_subtasks();
|
|
5935
6046
|
init_tide();
|
|
5936
6047
|
init_format();
|
|
5937
6048
|
init_raid();
|
|
@@ -6002,7 +6113,7 @@ function myRaid(data, personId) {
|
|
|
6002
6113
|
/** A project's board: its visible columns, in order, with their tasks. */
|
|
6003
6114
|
function boardOf(data, projectId) {
|
|
6004
6115
|
const config = boardConfigOf(data, projectId);
|
|
6005
|
-
const tasks = tasksOf(data.tasks, projectId);
|
|
6116
|
+
const tasks = topLevel(tasksOf(data.tasks, projectId), data.tasks);
|
|
6006
6117
|
return visibleColumns(config).map((col) => ({
|
|
6007
6118
|
status: col.status,
|
|
6008
6119
|
label: columnLabel(col),
|
|
@@ -6018,7 +6129,7 @@ function neighbourStatus(data, projectId, status, step) {
|
|
|
6018
6129
|
return cols[at + step] ?? null;
|
|
6019
6130
|
}
|
|
6020
6131
|
function projectTasks(data, projectId) {
|
|
6021
|
-
return
|
|
6132
|
+
return topLevel(tasksOf(data.tasks, projectId), data.tasks).sort((a, b) => STATUS_ORDER.indexOf(a.status) - STATUS_ORDER.indexOf(b.status) || (a.number ?? 0) - (b.number ?? 0));
|
|
6022
6133
|
}
|
|
6023
6134
|
/**
|
|
6024
6135
|
* The project a new task goes in when none was picked: the only open one,
|
|
@@ -19775,11 +19886,7 @@ function fieldValueText(def, value, nameOf) {
|
|
|
19775
19886
|
}
|
|
19776
19887
|
|
|
19777
19888
|
// src/lib/exports.ts
|
|
19778
|
-
|
|
19779
|
-
* The spreadsheets Harbor hands out: a project's tasks, the risk register and
|
|
19780
|
-
* the review. The website downloads them, agents and the terminal get the
|
|
19781
|
-
* same text (the export_csv tool, `harbor export`).
|
|
19782
|
-
*/
|
|
19889
|
+
init_subtasks();
|
|
19783
19890
|
init_derived_health();
|
|
19784
19891
|
init_priority();
|
|
19785
19892
|
init_raid();
|
|
@@ -19814,6 +19921,7 @@ function tasksCsv(data, projectId) {
|
|
|
19814
19921
|
"Done",
|
|
19815
19922
|
"Blocked by",
|
|
19816
19923
|
"Description",
|
|
19924
|
+
"Subtask of",
|
|
19817
19925
|
...fields.map((f) => f.name)
|
|
19818
19926
|
], tasks.map((t) => [
|
|
19819
19927
|
taskKey(t, project) ?? "",
|
|
@@ -19825,6 +19933,7 @@ function tasksCsv(data, projectId) {
|
|
|
19825
19933
|
t.status === "done" ? "yes" : "no",
|
|
19826
19934
|
blockerIds(t).map((id) => here.get(id)).filter((b) => Boolean(b)).map(name).join("; "),
|
|
19827
19935
|
t.description,
|
|
19936
|
+
((p) => p ? name(p) : "")(parentOf(t, data.tasks)),
|
|
19828
19937
|
...fields.map((f) => f.type === "url" ? safeLink(t.customFieldValues?.[f.id]) ?? "" : fieldValueText(f, t.customFieldValues?.[f.id], nameOf))
|
|
19829
19938
|
]));
|
|
19830
19939
|
}
|
|
@@ -20435,16 +20544,7 @@ function approvalsWaitingOn(tasks, personId) {
|
|
|
20435
20544
|
}
|
|
20436
20545
|
|
|
20437
20546
|
// src/lib/mcp/tools.ts
|
|
20438
|
-
|
|
20439
|
-
* What an agent can read about a harbor, as MCP tools. Each tool is a pure
|
|
20440
|
-
* function of one harbor's document (health already stamped) and who is
|
|
20441
|
-
* asking, so it is tested against the demo without a server. The route in
|
|
20442
|
-
* src/routes/api/mcp.ts signs the caller in, picks the harbor and loads it.
|
|
20443
|
-
*
|
|
20444
|
-
* Read only: nothing here changes a workspace. Answers carry ids so an agent
|
|
20445
|
-
* can ask about one thing next, and every list is capped and pageable, since
|
|
20446
|
-
* a harbor can hold thousands of tasks.
|
|
20447
|
-
*/
|
|
20547
|
+
init_subtasks();
|
|
20448
20548
|
init_thanks();
|
|
20449
20549
|
init_task_keys();
|
|
20450
20550
|
init_brief();
|
|
@@ -21202,12 +21302,13 @@ const HARBOR_TOOLS = [
|
|
|
21202
21302
|
if (input.query) {
|
|
21203
21303
|
const q = input.query;
|
|
21204
21304
|
const named = findByKey(data, q);
|
|
21305
|
+
const names = new Map(data.projects.map((p) => [p.id, p.name]));
|
|
21205
21306
|
list = list.map((t) => ({
|
|
21206
21307
|
t,
|
|
21207
|
-
s: scoreMatch(q, t.title, t.description)
|
|
21308
|
+
s: scoreMatch(q, t.title, t.description, names.get(t.projectId))
|
|
21208
21309
|
})).filter((h) => h.s > 0 && h.t !== named).sort((a, b) => b.s - a.s).map((h) => h.t);
|
|
21209
21310
|
if (named) list.unshift(named);
|
|
21210
|
-
if (!input.status?.length) doneMatches = data.tasks.filter((t) => t.status === "done" && t !== named && fits(t) && scoreMatch(q, t.title, t.description) > 0).length;
|
|
21311
|
+
if (!input.status?.length) doneMatches = data.tasks.filter((t) => t.status === "done" && t !== named && fits(t) && scoreMatch(q, t.title, t.description, names.get(t.projectId)) > 0).length;
|
|
21211
21312
|
} else list = [...list].sort((a, b) => a.status === "done" && b.status === "done" ? (b.completedAt ?? "").localeCompare(a.completedAt ?? "") : Number(a.status === "done") - Number(b.status === "done") || (a.dueDate ?? "9999").localeCompare(b.dueDate ?? "9999"));
|
|
21212
21313
|
return {
|
|
21213
21314
|
...doneMatches ? { doneMatches } : {},
|
|
@@ -21219,7 +21320,7 @@ const HARBOR_TOOLS = [
|
|
|
21219
21320
|
tool({
|
|
21220
21321
|
name: "get_task",
|
|
21221
21322
|
title: "Task details",
|
|
21222
|
-
description: "One task: its owner (assignee) and anyone also on it (alsoOn), notes, checklist, its files, what it waits on, what waits on it, its latest comments, the earliest due date its owner's queue suggests (pilotDue: fits says whether the due date holds; absent with no owner, an agent owner, or once done), its place in its board column (`position`, from 1 at the top), time logged on it (timeLogged: the total and the latest entries, when there is any), its values for the project's own fields by name (fields, when it has any; set them with set_field), and its url on the website." + DATA_NOTE,
|
|
21323
|
+
description: "One task: its owner (assignee) and anyone also on it (alsoOn), notes, checklist, its files, what it waits on, what waits on it, its latest comments, the earliest due date its owner's queue suggests (pilotDue: fits says whether the due date holds; absent with no owner, an agent owner, or once done), its place in its board column (`position`, from 1 at the top), time logged on it (timeLogged: the total and the latest entries, when there is any), its values for the project's own fields by name (fields, when it has any; set them with set_field), its subtasks or the task it's a subtask of (subtaskOf), and its url on the website." + DATA_NOTE,
|
|
21223
21324
|
input: object({
|
|
21224
21325
|
harbor: harborArg,
|
|
21225
21326
|
task: string().min(1).max(100).describe("Task id or key (\"RR-42\"), from search_tasks or my_work.")
|
|
@@ -21271,6 +21372,12 @@ const HARBOR_TOOLS = [
|
|
|
21271
21372
|
...t.time?.length ? { timeLogged: timeOut(t, people) } : {},
|
|
21272
21373
|
waitingOn: blockersOf(t, data.tasks).map(brief),
|
|
21273
21374
|
waitedOnBy: waiters.map(brief),
|
|
21375
|
+
...((parent) => parent ? { subtaskOf: brief(parent) } : {})(parentOf(t, data.tasks)),
|
|
21376
|
+
...((subs) => subs.length ? { subtasks: subs.map((x) => ({
|
|
21377
|
+
...brief(x),
|
|
21378
|
+
assignee: nameOf$2(people, x.assigneeId),
|
|
21379
|
+
due: dayOf(x.dueDate)
|
|
21380
|
+
})) } : {})(subtasksOf(t, data.tasks)),
|
|
21274
21381
|
...t.sounding ? { sounding: soundingOut(data, t, me(ctx)?.id ?? null, ctx.now) } : {},
|
|
21275
21382
|
...t.approval ? { approval: approvalOut(data, t.approval) } : {},
|
|
21276
21383
|
...((v) => v ? { fields: v } : {})(fieldValuesOut(data, project, t)),
|
|
@@ -22878,7 +22985,10 @@ function withStatus(task, status) {
|
|
|
22878
22985
|
touchedAt: stamp()
|
|
22879
22986
|
};
|
|
22880
22987
|
if (status === "done") next.completedAt = stamp();
|
|
22881
|
-
else if (task.status === "done")
|
|
22988
|
+
else if (task.status === "done") {
|
|
22989
|
+
next.completedAt = null;
|
|
22990
|
+
if (task.approval?.status === "approved") next.approval = null;
|
|
22991
|
+
}
|
|
22882
22992
|
return next;
|
|
22883
22993
|
}
|
|
22884
22994
|
var init_shared = __esmMin((() => {}));
|
|
@@ -23051,6 +23161,26 @@ var init_session = __esmMin((() => {
|
|
|
23051
23161
|
init_format();
|
|
23052
23162
|
}));
|
|
23053
23163
|
|
|
23164
|
+
// src/lib/assignment.ts
|
|
23165
|
+
/**
|
|
23166
|
+
* Record who set a task's assignee and when. Only a change of assignee
|
|
23167
|
+
* stamps; clearing the assignee clears the stamp.
|
|
23168
|
+
*/
|
|
23169
|
+
function stampAssignment(before, next, by, at) {
|
|
23170
|
+
if (before && before.assigneeId === next.assigneeId) return next;
|
|
23171
|
+
if (!next.assigneeId) return {
|
|
23172
|
+
...next,
|
|
23173
|
+
assignedBy: null,
|
|
23174
|
+
assignedAt: null
|
|
23175
|
+
};
|
|
23176
|
+
return {
|
|
23177
|
+
...next,
|
|
23178
|
+
assignedBy: by,
|
|
23179
|
+
assignedAt: at
|
|
23180
|
+
};
|
|
23181
|
+
}
|
|
23182
|
+
var init_assignment = __esmMin((() => {}));
|
|
23183
|
+
|
|
23054
23184
|
// src/lib/first-week.ts
|
|
23055
23185
|
/** The program launch named for you, when nobody has renamed it yet. */
|
|
23056
23186
|
function starterProgram(data) {
|
|
@@ -23377,13 +23507,13 @@ function hierarchySlice({ get, write }) {
|
|
|
23377
23507
|
const program = get().programs.find((p) => p.id === programId);
|
|
23378
23508
|
get().log(`moved ${ids.length} project${ids.length === 1 ? "" : "s"} to ${program?.name ?? "a program"}`, `/programs/${programId}`);
|
|
23379
23509
|
},
|
|
23380
|
-
restoreRows: (rows) => {
|
|
23510
|
+
restoreRows: (rows, links) => {
|
|
23381
23511
|
const state = get();
|
|
23382
23512
|
const back = (current, add) => {
|
|
23383
23513
|
const have = new Set((current ?? []).map((r) => r.id));
|
|
23384
23514
|
return [...current ?? [], ...(add ?? []).filter((r) => !have.has(r.id))];
|
|
23385
23515
|
};
|
|
23386
|
-
|
|
23516
|
+
const next = {
|
|
23387
23517
|
portfolios: back(state.portfolios, rows.portfolios),
|
|
23388
23518
|
programs: back(state.programs, rows.programs),
|
|
23389
23519
|
projects: back(state.projects, rows.projects),
|
|
@@ -23399,7 +23529,14 @@ function hierarchySlice({ get, write }) {
|
|
|
23399
23529
|
...rows.boardConfigs ?? {},
|
|
23400
23530
|
...state.boardConfigs ?? {}
|
|
23401
23531
|
}
|
|
23402
|
-
}
|
|
23532
|
+
};
|
|
23533
|
+
write(links ? {
|
|
23534
|
+
...next,
|
|
23535
|
+
...relink({
|
|
23536
|
+
...state,
|
|
23537
|
+
...next
|
|
23538
|
+
}, links)
|
|
23539
|
+
} : next);
|
|
23403
23540
|
const name = rows.portfolios?.[0]?.name ?? rows.programs?.[0]?.name ?? rows.projects?.[0]?.name;
|
|
23404
23541
|
if (name) get().log(`put back “${name}”`);
|
|
23405
23542
|
},
|
|
@@ -23410,10 +23547,13 @@ function hierarchySlice({ get, write }) {
|
|
|
23410
23547
|
const ids = have(current);
|
|
23411
23548
|
return [...current, ...(more ?? []).filter((r) => !ids.has(r.id))];
|
|
23412
23549
|
};
|
|
23550
|
+
const small = (rows.tasks?.length ?? 0) <= 20;
|
|
23551
|
+
const at = stamp();
|
|
23552
|
+
const incoming = small ? rows.tasks?.map((t) => stampAssignment(void 0, t, state.currentUserId, at)) : rows.tasks;
|
|
23413
23553
|
write({
|
|
23414
23554
|
programs: add(state.programs, rows.programs),
|
|
23415
23555
|
projects: add(state.projects, rows.projects),
|
|
23416
|
-
tasks: add(state.tasks,
|
|
23556
|
+
tasks: add(state.tasks, incoming),
|
|
23417
23557
|
people: add(state.people, rows.people),
|
|
23418
23558
|
raidItems: add(state.raidItems, rows.raidItems)
|
|
23419
23559
|
});
|
|
@@ -23484,6 +23624,7 @@ function hierarchySlice({ get, write }) {
|
|
|
23484
23624
|
}
|
|
23485
23625
|
};
|
|
23486
23626
|
}
|
|
23627
|
+
var SMALL_IMPORT;
|
|
23487
23628
|
var init_hierarchy = __esmMin((() => {
|
|
23488
23629
|
init_board();
|
|
23489
23630
|
init_cascade();
|
|
@@ -23493,11 +23634,13 @@ var init_hierarchy = __esmMin((() => {
|
|
|
23493
23634
|
init_health_history();
|
|
23494
23635
|
init_derived_health();
|
|
23495
23636
|
init_shared();
|
|
23637
|
+
init_assignment();
|
|
23496
23638
|
init_hill();
|
|
23497
23639
|
init_charter_lines();
|
|
23498
23640
|
init_format();
|
|
23499
23641
|
init_empty();
|
|
23500
23642
|
init_first_week();
|
|
23643
|
+
SMALL_IMPORT = 20;
|
|
23501
23644
|
}));
|
|
23502
23645
|
|
|
23503
23646
|
// src/lib/github-refs.ts
|
|
@@ -23546,6 +23689,16 @@ function runActions(actions, start, ctx) {
|
|
|
23546
23689
|
for (const action of actions) switch (action.type) {
|
|
23547
23690
|
case "set_status": {
|
|
23548
23691
|
if (!action.status) break;
|
|
23692
|
+
if (action.status === "done" && task.status !== "done" && ctx.waiting?.(task)) {
|
|
23693
|
+
comments.push({
|
|
23694
|
+
taskId: task.id,
|
|
23695
|
+
authorId: ctx.authorId,
|
|
23696
|
+
body: "Left open: this still waits on unfinished work, so the rule didn’t finish it.",
|
|
23697
|
+
createdAt: ctx.now,
|
|
23698
|
+
mentionedIds: []
|
|
23699
|
+
});
|
|
23700
|
+
break;
|
|
23701
|
+
}
|
|
23549
23702
|
const wasDone = task.status === "done";
|
|
23550
23703
|
const completedAt = action.status === "done" ? task.completedAt ?? ctx.now : wasDone ? null : task.completedAt;
|
|
23551
23704
|
task = {
|
|
@@ -23611,7 +23764,8 @@ function applyAutomations(project, prev, next, opts) {
|
|
|
23611
23764
|
const ctx = {
|
|
23612
23765
|
authorId: opts.currentUserId,
|
|
23613
23766
|
now,
|
|
23614
|
-
today
|
|
23767
|
+
today,
|
|
23768
|
+
waiting: opts.waiting
|
|
23615
23769
|
};
|
|
23616
23770
|
let task = next;
|
|
23617
23771
|
const comments = [];
|
|
@@ -23717,26 +23871,6 @@ var init_recurrence = __esmMin((() => {
|
|
|
23717
23871
|
step = (d, repeat, n) => repeat === "weekly" ? addDays(d, 7 * n) : repeat === "biweekly" ? addDays(d, 14 * n) : addMonths$1(d, n);
|
|
23718
23872
|
}));
|
|
23719
23873
|
|
|
23720
|
-
// src/lib/assignment.ts
|
|
23721
|
-
/**
|
|
23722
|
-
* Record who set a task's assignee and when. Only a change of assignee
|
|
23723
|
-
* stamps; clearing the assignee clears the stamp.
|
|
23724
|
-
*/
|
|
23725
|
-
function stampAssignment(before, next, by, at) {
|
|
23726
|
-
if (before && before.assigneeId === next.assigneeId) return next;
|
|
23727
|
-
if (!next.assigneeId) return {
|
|
23728
|
-
...next,
|
|
23729
|
-
assignedBy: null,
|
|
23730
|
-
assignedAt: null
|
|
23731
|
-
};
|
|
23732
|
-
return {
|
|
23733
|
-
...next,
|
|
23734
|
-
assignedBy: by,
|
|
23735
|
-
assignedAt: at
|
|
23736
|
-
};
|
|
23737
|
-
}
|
|
23738
|
-
var init_assignment = __esmMin((() => {}));
|
|
23739
|
-
|
|
23740
23874
|
// src/lib/bulk.ts
|
|
23741
23875
|
/** A task's due date moved by `days`, or null when it has none. */
|
|
23742
23876
|
function shiftedDue(due, days) {
|
|
@@ -23813,7 +23947,9 @@ function tasksSlice({ set, get, write }) {
|
|
|
23813
23947
|
get().log(`added ${who?.name ?? "someone"} as a second pair of hands on ${taskIds.length + raidIds.length} item${taskIds.length + raidIds.length === 1 ? "" : "s"}`, "/people");
|
|
23814
23948
|
},
|
|
23815
23949
|
addTask: (t, opts) => {
|
|
23816
|
-
|
|
23950
|
+
const wanted = t.parentTaskId ? byId(get().tasks).get(t.parentTaskId) : void 0;
|
|
23951
|
+
const parentTaskId = wanted && wanted.projectId === t.projectId && !subtaskProblem("", wanted.id, get().tasks) ? wanted.id : null;
|
|
23952
|
+
if (!opts?.allowOverCap && !parentTaskId) {
|
|
23817
23953
|
if (columnWip(get(), t.projectId, t.status, 1)) return "";
|
|
23818
23954
|
}
|
|
23819
23955
|
const id = uid("t");
|
|
@@ -23824,6 +23960,7 @@ function tasksSlice({ set, get, write }) {
|
|
|
23824
23960
|
createdById: get().currentUserId || null,
|
|
23825
23961
|
completedAt: t.status === "done" ? t.completedAt ?? stamp() : null,
|
|
23826
23962
|
checklist: t.checklist ?? [],
|
|
23963
|
+
..."parentTaskId" in t ? { parentTaskId } : {},
|
|
23827
23964
|
...blockersPatch(blockerIds({
|
|
23828
23965
|
id,
|
|
23829
23966
|
blockedById: t.blockedById,
|
|
@@ -23833,7 +23970,8 @@ function tasksSlice({ set, get, write }) {
|
|
|
23833
23970
|
const project = get().projects.find((p) => p.id === t.projectId);
|
|
23834
23971
|
const { task: automated, comments: autoComments } = applyAutomations(project, void 0, row, {
|
|
23835
23972
|
created: true,
|
|
23836
|
-
currentUserId: get().currentUserId
|
|
23973
|
+
currentUserId: get().currentUserId,
|
|
23974
|
+
waiting: (t) => isBlocked$1(t, get().tasks)
|
|
23837
23975
|
});
|
|
23838
23976
|
const finalRow = stampAssignment(void 0, automated, get().currentUserId, stamp());
|
|
23839
23977
|
write({
|
|
@@ -23849,6 +23987,10 @@ function tasksSlice({ set, get, write }) {
|
|
|
23849
23987
|
},
|
|
23850
23988
|
updateTask: (id, patch, opts) => {
|
|
23851
23989
|
const task = get().tasks.find((row) => row.id === id);
|
|
23990
|
+
if (task && patch.parentTaskId && subtaskProblem(id, patch.parentTaskId, get().tasks)) {
|
|
23991
|
+
patch = { ...patch };
|
|
23992
|
+
delete patch.parentTaskId;
|
|
23993
|
+
}
|
|
23852
23994
|
if (task && patch.status && patch.status !== task.status) {
|
|
23853
23995
|
if (patch.status === "done" && !opts?.allowBlocked && isBlocked$1(task, get().tasks)) {
|
|
23854
23996
|
set({ blockPending: {
|
|
@@ -23860,13 +24002,24 @@ function tasksSlice({ set, get, write }) {
|
|
|
23860
24002
|
} });
|
|
23861
24003
|
return;
|
|
23862
24004
|
}
|
|
23863
|
-
if (!opts?.allowOverCap) {
|
|
24005
|
+
if (!opts?.allowOverCap && !isSubtask(task, get().tasks)) {
|
|
23864
24006
|
if (columnWip(get(), task.projectId, patch.status, 1)) return;
|
|
23865
24007
|
}
|
|
23866
24008
|
}
|
|
24009
|
+
const moving = task && patch.projectId && patch.projectId !== task.projectId;
|
|
24010
|
+
const carried = moving ? new Set(subtasksOf(task, get().tasks).map((t) => t.id)) : null;
|
|
24011
|
+
if (moving && task.parentTaskId && !("parentTaskId" in patch)) patch = {
|
|
24012
|
+
...patch,
|
|
24013
|
+
parentTaskId: null
|
|
24014
|
+
};
|
|
23867
24015
|
const project = get().projects.find((p) => p.id === task?.projectId);
|
|
23868
24016
|
const newComments = [];
|
|
23869
24017
|
const tasks = get().tasks.map((row) => {
|
|
24018
|
+
if (carried?.has(row.id)) return {
|
|
24019
|
+
...row,
|
|
24020
|
+
projectId: patch.projectId,
|
|
24021
|
+
touchedAt: stamp()
|
|
24022
|
+
};
|
|
23870
24023
|
if (row.id !== id) return row;
|
|
23871
24024
|
const next = {
|
|
23872
24025
|
...row,
|
|
@@ -23876,12 +24029,16 @@ function tasksSlice({ set, get, write }) {
|
|
|
23876
24029
|
if (next.assigneeId && next.followerIds?.includes(next.assigneeId)) next.followerIds = alsoOnIds(next);
|
|
23877
24030
|
if (patch.status && patch.status !== row.status) {
|
|
23878
24031
|
if (patch.status === "done" && !patch.completedAt) next.completedAt = stamp();
|
|
23879
|
-
else if (row.status === "done" && patch.status !== "done")
|
|
24032
|
+
else if (row.status === "done" && patch.status !== "done") {
|
|
24033
|
+
next.completedAt = null;
|
|
24034
|
+
if (row.approval?.status === "approved" && !("approval" in patch)) next.approval = null;
|
|
24035
|
+
}
|
|
23880
24036
|
if (patch.status === "done" && isBlocked$1(row, get().tasks)) Object.assign(next, blockersPatch([]));
|
|
23881
24037
|
}
|
|
23882
24038
|
const { task: finalTask, comments: autoComments } = applyAutomations(project, row, next, {
|
|
23883
24039
|
created: false,
|
|
23884
|
-
currentUserId: get().currentUserId
|
|
24040
|
+
currentUserId: get().currentUserId,
|
|
24041
|
+
waiting: (t) => isBlocked$1(t, get().tasks)
|
|
23885
24042
|
});
|
|
23886
24043
|
newComments.push(...autoComments.map((c) => ({
|
|
23887
24044
|
...c,
|
|
@@ -23912,7 +24069,7 @@ function tasksSlice({ set, get, write }) {
|
|
|
23912
24069
|
} });
|
|
23913
24070
|
return;
|
|
23914
24071
|
}
|
|
23915
|
-
if (!opts?.allowOverCap) {
|
|
24072
|
+
if (!opts?.allowOverCap && !isSubtask(task, get().tasks)) {
|
|
23916
24073
|
if (columnWip(get(), task.projectId, status, 1)) return;
|
|
23917
24074
|
}
|
|
23918
24075
|
const nextRow = withStatus(task, status);
|
|
@@ -23920,7 +24077,8 @@ function tasksSlice({ set, get, write }) {
|
|
|
23920
24077
|
const project = get().projects.find((p) => p.id === task.projectId);
|
|
23921
24078
|
const { task: finalRow, comments: autoComments } = applyAutomations(project, task, nextRow, {
|
|
23922
24079
|
created: false,
|
|
23923
|
-
currentUserId: get().currentUserId
|
|
24080
|
+
currentUserId: get().currentUserId,
|
|
24081
|
+
waiting: (t) => isBlocked$1(t, get().tasks)
|
|
23924
24082
|
});
|
|
23925
24083
|
const follow = status === "done" ? nextOccurrence(finalRow, uid("t"), todayISO(), stamp(), tasksOf(get().tasks, task.projectId)) : null;
|
|
23926
24084
|
const moved = get().tasks.map((row) => row.id === id ? finalRow : row);
|
|
@@ -24002,7 +24160,8 @@ function tasksSlice({ set, get, write }) {
|
|
|
24002
24160
|
},
|
|
24003
24161
|
updateComment: (id, body) => {
|
|
24004
24162
|
const comment = get().comments.find((c) => c.id === id);
|
|
24005
|
-
if (!comment
|
|
24163
|
+
if (!comment) return;
|
|
24164
|
+
if (comment.authorId !== get().currentUserId && get().myRole !== "owner") return;
|
|
24006
24165
|
const trimmed = body.trim();
|
|
24007
24166
|
if (!trimmed) return;
|
|
24008
24167
|
const mentionedIds = parseMentions(trimmed, get().people).filter((pid) => pid !== get().currentUserId);
|
|
@@ -24015,7 +24174,8 @@ function tasksSlice({ set, get, write }) {
|
|
|
24015
24174
|
},
|
|
24016
24175
|
deleteComment: (id) => {
|
|
24017
24176
|
const comment = get().comments.find((c) => c.id === id);
|
|
24018
|
-
if (!comment
|
|
24177
|
+
if (!comment) return;
|
|
24178
|
+
if (comment.authorId !== get().currentUserId && get().myRole !== "owner") return;
|
|
24019
24179
|
set({ comments: get().comments.filter((c) => c.id !== id) });
|
|
24020
24180
|
},
|
|
24021
24181
|
addChecklistItem: (taskId, title) => {
|
|
@@ -24196,7 +24356,8 @@ function tasksSlice({ set, get, write }) {
|
|
|
24196
24356
|
}
|
|
24197
24357
|
const { task: automated, comments } = applyAutomations(projects.get(row.projectId), row, next, {
|
|
24198
24358
|
created: false,
|
|
24199
|
-
currentUserId: me
|
|
24359
|
+
currentUserId: me,
|
|
24360
|
+
waiting: (t) => isBlocked$1(t, s.tasks)
|
|
24200
24361
|
});
|
|
24201
24362
|
newComments.push(...comments.map((c) => ({
|
|
24202
24363
|
...c,
|
|
@@ -24328,6 +24489,7 @@ var init_tasks = __esmMin((() => {
|
|
|
24328
24489
|
init_assignment();
|
|
24329
24490
|
init_bulk();
|
|
24330
24491
|
init_task_people();
|
|
24492
|
+
init_subtasks();
|
|
24331
24493
|
}));
|
|
24332
24494
|
|
|
24333
24495
|
// src/lib/raid-history.ts
|
|
@@ -30387,6 +30549,7 @@ init_clock();
|
|
|
30387
30549
|
init_date_text();
|
|
30388
30550
|
init_work();
|
|
30389
30551
|
init_blockers();
|
|
30552
|
+
init_subtasks();
|
|
30390
30553
|
init_recurrence();
|
|
30391
30554
|
init_task_people();
|
|
30392
30555
|
init_search();
|
|
@@ -30505,6 +30668,8 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
|
|
|
30505
30668
|
const thread = comments.filter((c) => c.taskId === task.id).slice(-4);
|
|
30506
30669
|
const mine = comments.filter((c) => c.taskId === task.id && c.authorId === me);
|
|
30507
30670
|
const blockers = blockersOf(task, tasks);
|
|
30671
|
+
const subtaskOf = parentOf(task, tasks);
|
|
30672
|
+
const subs = subtasksOf(task, tasks);
|
|
30508
30673
|
const done = () => setEditing(null);
|
|
30509
30674
|
const nameOf = (id) => people.find((p) => p.id === id)?.name;
|
|
30510
30675
|
const setCustom = (fieldId, value) => store.getState().setTaskCustomFieldValue(task.id, fieldId, value);
|
|
@@ -30857,6 +31022,40 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
|
|
|
30857
31022
|
]
|
|
30858
31023
|
}, item.id);
|
|
30859
31024
|
}),
|
|
31025
|
+
subtaskOf ? /* @__PURE__ */ jsxs(Text, {
|
|
31026
|
+
color: theme.muted,
|
|
31027
|
+
wrap: "truncate-end",
|
|
31028
|
+
children: [
|
|
31029
|
+
" ",
|
|
31030
|
+
"Subtask of ",
|
|
31031
|
+
keyOf$1({ projects }, subtaskOf),
|
|
31032
|
+
" ",
|
|
31033
|
+
subtaskOf.title
|
|
31034
|
+
]
|
|
31035
|
+
}) : null,
|
|
31036
|
+
subs.length > 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
31037
|
+
color: theme.muted,
|
|
31038
|
+
children: [
|
|
31039
|
+
" ",
|
|
31040
|
+
"Subtasks ",
|
|
31041
|
+
subs.filter((x) => x.status === "done").length,
|
|
31042
|
+
"/",
|
|
31043
|
+
subs.length
|
|
31044
|
+
]
|
|
31045
|
+
}) : null,
|
|
31046
|
+
subs.map((x) => /* @__PURE__ */ jsxs(Text, {
|
|
31047
|
+
wrap: "truncate-end",
|
|
31048
|
+
children: [
|
|
31049
|
+
" ",
|
|
31050
|
+
x.status === "done" ? "[x] " : "[ ] ",
|
|
31051
|
+
keyOf$1({ projects }, x),
|
|
31052
|
+
" ",
|
|
31053
|
+
x.status === "done" ? /* @__PURE__ */ jsx(Text, {
|
|
31054
|
+
strikethrough: true,
|
|
31055
|
+
children: x.title
|
|
31056
|
+
}) : x.title
|
|
31057
|
+
]
|
|
31058
|
+
}, x.id)),
|
|
30860
31059
|
(task.github ?? []).slice(0, 3).map((ref) => /* @__PURE__ */ jsxs(Text, {
|
|
30861
31060
|
color: theme.muted,
|
|
30862
31061
|
wrap: "truncate-end",
|
package/lib/cli.mjs
CHANGED
|
@@ -168,7 +168,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
168
168
|
[--also "NAME, NAME"] [--due DATE|pilot]
|
|
169
169
|
[--priority low|medium|high|urgent]
|
|
170
170
|
[--status S] [--comment TEXT] [--over-limit] [--dry-run]
|
|
171
|
-
[--literal] [--template NAME]
|
|
171
|
+
[--literal] [--template NAME] [--parent TASK]
|
|
172
172
|
reads plain words in TITLE as the
|
|
173
173
|
website does: @name, by friday,
|
|
174
174
|
!high, #project (--literal: keep
|
|
@@ -183,7 +183,8 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
183
183
|
[--also "NAME, NAME"|none] [--due DATE|pilot|none]
|
|
184
184
|
[--priority P] [--status S] [--notes TEXT]
|
|
185
185
|
[--repeat weekly|biweekly|monthly|never]
|
|
186
|
-
[--waits-on TASK,TASK|none] [--
|
|
186
|
+
[--waits-on TASK,TASK|none] [--parent TASK|none]
|
|
187
|
+
[--confirm] [--over-limit]
|
|
187
188
|
harbor limit PROJECT [COLUMN N|none] how many tasks a board column
|
|
188
189
|
takes at once; the owner sets it
|
|
189
190
|
harbor week [--next|--last|--week DATE] [--review]
|
|
@@ -704,7 +705,7 @@ export function hint(message, project) {
|
|
|
704
705
|
)
|
|
705
706
|
.replace(
|
|
706
707
|
/^Nothing to change: pass a field to update\.$/,
|
|
707
|
-
"Nothing to change. Say what with --title, --assignee, --also, --due, --priority, --status, --notes, --repeat
|
|
708
|
+
"Nothing to change. Say what with --title, --assignee, --also, --due, --priority, --status, --notes, --repeat, --waits-on or --parent.",
|
|
708
709
|
)
|
|
709
710
|
.replace(
|
|
710
711
|
/^This connection was allowed to read only\..*$/s,
|
|
@@ -785,6 +786,10 @@ const DETAIL = {
|
|
|
785
786
|
" notes, priority, owner, others on it, checklist and due offset fill what",
|
|
786
787
|
" the flags don't. Leave out TITLE to take the template's.",
|
|
787
788
|
"",
|
|
789
|
+
" --parent TASK adds it inside that task as a subtask: a whole task with its",
|
|
790
|
+
" own owner and date, shown in the parent's details rather than on the",
|
|
791
|
+
" board. It goes in the parent's project; subtasks go one level deep.",
|
|
792
|
+
"",
|
|
788
793
|
" --due pilot takes the earliest day the owner's queue suggests: their open",
|
|
789
794
|
" work due first, their weekly pace, time away, what it waits on and how",
|
|
790
795
|
" long their similar tasks took. It needs an owner (--assignee none is",
|
|
@@ -2288,6 +2293,7 @@ export async function run(argv, io = {}) {
|
|
|
2288
2293
|
),
|
|
2289
2294
|
];
|
|
2290
2295
|
if (t.alsoOn?.length) lines.push(`Also on it: ${t.alsoOn.join(", ")}.`);
|
|
2296
|
+
if (t.subtaskOf) lines.push(`Subtask of ${t.subtaskOf.key ?? t.subtaskOf.id} ${t.subtaskOf.title}.`);
|
|
2291
2297
|
if (t.repeats) lines.push(`Repeats ${REPEAT_WORD[t.repeats] ?? t.repeats}.`);
|
|
2292
2298
|
for (const [name, v] of Object.entries(t.fields ?? {}))
|
|
2293
2299
|
lines.push(`${name}: ${Array.isArray(v) ? v.join(", ") : v === true ? "yes" : v}`);
|
|
@@ -2296,6 +2302,15 @@ export async function run(argv, io = {}) {
|
|
|
2296
2302
|
if (t.description) lines.push("", t.description);
|
|
2297
2303
|
if (t.checklist.length)
|
|
2298
2304
|
lines.push("", ...t.checklist.map((c) => ` [${c.done ? "x" : " "}] ${c.step}`));
|
|
2305
|
+
if (t.subtasks?.length)
|
|
2306
|
+
lines.push(
|
|
2307
|
+
"",
|
|
2308
|
+
`Subtasks (${t.subtasks.filter((x) => x.status === "done").length}/${t.subtasks.length} done):`,
|
|
2309
|
+
...t.subtasks.map(
|
|
2310
|
+
(x) =>
|
|
2311
|
+
` [${x.status === "done" ? "x" : " "}] ${x.key ?? x.id} ${x.title}${x.assignee ? `, ${x.assignee}` : ""}${x.due ? `, due ${x.due}` : ""}`,
|
|
2312
|
+
),
|
|
2313
|
+
);
|
|
2299
2314
|
if (t.waitingOn.length)
|
|
2300
2315
|
lines.push(
|
|
2301
2316
|
"",
|
|
@@ -2531,9 +2546,12 @@ export async function run(argv, io = {}) {
|
|
|
2531
2546
|
const { positionals, values, harbor } = parse(
|
|
2532
2547
|
spec(
|
|
2533
2548
|
["over-limit", "dry-run", "literal"],
|
|
2534
|
-
["project", "assignee", "also", "due", "priority", "status", "comment", "template"],
|
|
2549
|
+
["project", "assignee", "also", "due", "priority", "status", "comment", "template", "parent"],
|
|
2535
2550
|
),
|
|
2536
2551
|
);
|
|
2552
|
+
// A subtask goes in its parent's project.
|
|
2553
|
+
if (values.parent && !values.project)
|
|
2554
|
+
values.project = (await client.call("get_task", { harbor, task: values.parent })).projectId;
|
|
2537
2555
|
// --template with no title: the template's own title.
|
|
2538
2556
|
const fromTemplate = values.template !== undefined && positionals.length === 0;
|
|
2539
2557
|
// Several at once: one title a line on stdin, with no title (or -) after add.
|
|
@@ -2636,6 +2654,7 @@ export async function run(argv, io = {}) {
|
|
|
2636
2654
|
suggestedDue: duePilot(values.due),
|
|
2637
2655
|
priority: values.priority ?? words.priority?.value,
|
|
2638
2656
|
status: values.status === undefined ? undefined : statusWord(values.status),
|
|
2657
|
+
parent: values.parent,
|
|
2639
2658
|
overLimit: values["over-limit"] || undefined,
|
|
2640
2659
|
})
|
|
2641
2660
|
.catch((err) => {
|
|
@@ -2737,7 +2756,7 @@ export async function run(argv, io = {}) {
|
|
|
2737
2756
|
},
|
|
2738
2757
|
(r) =>
|
|
2739
2758
|
[
|
|
2740
|
-
`${r.updated.title}: ${{ doing: "started", review: "in review", done: "done" }[status]}.`,
|
|
2759
|
+
`${r.updated.title}: ${r.reopened ? "reopened, " : ""}${{ doing: "started", review: "in review", done: "done" }[status]}.`,
|
|
2741
2760
|
r.queuedNext ? `Next one: ${taskLine(r.queuedNext, opts)}` : "",
|
|
2742
2761
|
r.projectFinished
|
|
2743
2762
|
? `That was the last open task in ${r.projectFinished}. Mark the project Done in the app (harbor) or on the website.`
|
|
@@ -2761,6 +2780,7 @@ export async function run(argv, io = {}) {
|
|
|
2761
2780
|
"notes",
|
|
2762
2781
|
"repeat",
|
|
2763
2782
|
"waits-on",
|
|
2783
|
+
"parent",
|
|
2764
2784
|
],
|
|
2765
2785
|
),
|
|
2766
2786
|
);
|
|
@@ -2791,12 +2811,18 @@ export async function run(argv, io = {}) {
|
|
|
2791
2811
|
.split(",")
|
|
2792
2812
|
.map((x) => x.trim())
|
|
2793
2813
|
.filter(Boolean),
|
|
2814
|
+
parent: values.parent,
|
|
2794
2815
|
confirmed: values.confirm || undefined,
|
|
2795
2816
|
overLimit: values["over-limit"] || undefined,
|
|
2796
2817
|
},
|
|
2797
2818
|
(r) =>
|
|
2798
2819
|
[
|
|
2799
2820
|
`Updated: ${taskLine(r.updated, opts)}`,
|
|
2821
|
+
values.parent !== undefined
|
|
2822
|
+
? r.updated.subtaskOf
|
|
2823
|
+
? `A subtask of ${r.updated.subtaskOf}.`
|
|
2824
|
+
: "A task of its own now."
|
|
2825
|
+
: null,
|
|
2800
2826
|
values.also !== undefined
|
|
2801
2827
|
? r.updated.alsoOn?.length
|
|
2802
2828
|
? `Also on it: ${r.updated.alsoOn.join(", ")}.`
|