@harborppm/cli 0.79.0 → 0.80.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 +492 -54
- package/lib/cli.mjs +51 -8
- package/lib/format.mjs +24 -0
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -5913,7 +5913,7 @@ const STATUS_ORDER = [
|
|
|
5913
5913
|
"review",
|
|
5914
5914
|
"done"
|
|
5915
5915
|
];
|
|
5916
|
-
const PRIORITIES$
|
|
5916
|
+
const PRIORITIES$2 = [
|
|
5917
5917
|
"low",
|
|
5918
5918
|
"medium",
|
|
5919
5919
|
"high",
|
|
@@ -5938,7 +5938,7 @@ function isLate(task, today) {
|
|
|
5938
5938
|
function myOpenTasks(data, personId, today) {
|
|
5939
5939
|
const mine = data.tasks.filter((t) => t.assigneeId === personId && t.status !== "done");
|
|
5940
5940
|
const rank = (t) => isLate(t, today) ? 0 : t.dueDate ? 1 : 2;
|
|
5941
|
-
return mine.sort((a, b) => rank(a) - rank(b) || (a.dueDate ?? "").localeCompare(b.dueDate ?? "") || PRIORITIES$
|
|
5941
|
+
return mine.sort((a, b) => rank(a) - rank(b) || (a.dueDate ?? "").localeCompare(b.dueDate ?? "") || PRIORITIES$2.indexOf(b.priority) - PRIORITIES$2.indexOf(a.priority));
|
|
5942
5942
|
}
|
|
5943
5943
|
/** Others' open tasks that wait on one of this person's. */
|
|
5944
5944
|
function waitingOnMe(data, personId) {
|
|
@@ -13235,6 +13235,7 @@ const PAGES = {
|
|
|
13235
13235
|
projects: "/projects",
|
|
13236
13236
|
risks: "/risks",
|
|
13237
13237
|
goals: "/goals",
|
|
13238
|
+
reports: "/reports",
|
|
13238
13239
|
calendar: "/calendar",
|
|
13239
13240
|
people: "/people"
|
|
13240
13241
|
};
|
|
@@ -13759,6 +13760,269 @@ function keyResultText(kr, work) {
|
|
|
13759
13760
|
}
|
|
13760
13761
|
const fmt = (n) => Number.isInteger(n) ? n.toLocaleString("en-US") : String(Math.round(n * 100) / 100);
|
|
13761
13762
|
|
|
13763
|
+
// src/lib/reports.ts
|
|
13764
|
+
init_date_fns();
|
|
13765
|
+
init_date_text();
|
|
13766
|
+
init_indexes();
|
|
13767
|
+
init_swatches();
|
|
13768
|
+
/** Weeks a "by week" chart shows: back from this week, or ahead for due dates. */
|
|
13769
|
+
const WEEKS_BACK = 12;
|
|
13770
|
+
const WEEKS_AHEAD = 8;
|
|
13771
|
+
/** Bars past this many are summed into "Everything else". */
|
|
13772
|
+
const MAX_BARS = 12;
|
|
13773
|
+
const GROUPS = [
|
|
13774
|
+
"status",
|
|
13775
|
+
"owner",
|
|
13776
|
+
"priority",
|
|
13777
|
+
"project",
|
|
13778
|
+
"program",
|
|
13779
|
+
"week"
|
|
13780
|
+
];
|
|
13781
|
+
const MEASURE_LABEL = {
|
|
13782
|
+
open: "Open tasks",
|
|
13783
|
+
done: "Finished tasks",
|
|
13784
|
+
all: "All tasks",
|
|
13785
|
+
overdue: "Overdue tasks",
|
|
13786
|
+
hours: "Hours logged"
|
|
13787
|
+
};
|
|
13788
|
+
const GROUP_LABEL = {
|
|
13789
|
+
status: "Status",
|
|
13790
|
+
owner: "Owner",
|
|
13791
|
+
priority: "Priority",
|
|
13792
|
+
project: "Project",
|
|
13793
|
+
program: "Program",
|
|
13794
|
+
week: "Week"
|
|
13795
|
+
};
|
|
13796
|
+
/** The splits a measure can take: hours have no status or priority of their own. */
|
|
13797
|
+
function groupsFor(measure) {
|
|
13798
|
+
return measure === "hours" ? [
|
|
13799
|
+
"owner",
|
|
13800
|
+
"project",
|
|
13801
|
+
"program",
|
|
13802
|
+
"week"
|
|
13803
|
+
] : GROUPS;
|
|
13804
|
+
}
|
|
13805
|
+
const STATUS_FILL = {
|
|
13806
|
+
backlog: "var(--color-swatch-slate)",
|
|
13807
|
+
todo: "var(--color-swatch-sand)",
|
|
13808
|
+
doing: "var(--color-swatch-sea)",
|
|
13809
|
+
review: "var(--color-swatch-rust)",
|
|
13810
|
+
done: "var(--color-swatch-moss)"
|
|
13811
|
+
};
|
|
13812
|
+
const PRIORITY_FILL = {
|
|
13813
|
+
low: "var(--color-swatch-slate)",
|
|
13814
|
+
medium: "var(--color-swatch-sea)",
|
|
13815
|
+
high: "var(--color-swatch-sand)",
|
|
13816
|
+
urgent: "var(--color-swatch-coral)"
|
|
13817
|
+
};
|
|
13818
|
+
const PRIORITIES$1 = [
|
|
13819
|
+
"urgent",
|
|
13820
|
+
"high",
|
|
13821
|
+
"medium",
|
|
13822
|
+
"low"
|
|
13823
|
+
];
|
|
13824
|
+
/** Colours handed out in turn where a slice has none of its own. */
|
|
13825
|
+
const CYCLE = [
|
|
13826
|
+
"sea",
|
|
13827
|
+
"sand",
|
|
13828
|
+
"moss",
|
|
13829
|
+
"rust",
|
|
13830
|
+
"slate",
|
|
13831
|
+
"pine",
|
|
13832
|
+
"clay",
|
|
13833
|
+
"sage"
|
|
13834
|
+
];
|
|
13835
|
+
const swatchFill = (s) => `var(--color-swatch-${s})`;
|
|
13836
|
+
/** The Monday on or before a day or moment (ISO, read in local time), or "" when it isn't a date. */
|
|
13837
|
+
function mondayOf(day) {
|
|
13838
|
+
const d = parseIso(day.length > 10 ? day : `${day}T12:00:00`);
|
|
13839
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
13840
|
+
const back = (d.getDay() + 6) % 7;
|
|
13841
|
+
return format(addDays(d, -back), "yyyy-MM-dd");
|
|
13842
|
+
}
|
|
13843
|
+
/** The projects a chart reads: everything, or what sits under its scope. */
|
|
13844
|
+
function scopeOf(data, scopeId) {
|
|
13845
|
+
if (!scopeId) return {
|
|
13846
|
+
projectIds: null,
|
|
13847
|
+
name: "Whole harbor",
|
|
13848
|
+
found: true
|
|
13849
|
+
};
|
|
13850
|
+
const project = byId(data.projects).get(scopeId);
|
|
13851
|
+
if (project) return {
|
|
13852
|
+
projectIds: /* @__PURE__ */ new Set([project.id]),
|
|
13853
|
+
name: project.name,
|
|
13854
|
+
found: true
|
|
13855
|
+
};
|
|
13856
|
+
const program = byId(data.programs).get(scopeId);
|
|
13857
|
+
if (program) {
|
|
13858
|
+
const ids = data.projects.filter((p) => p.programId === program.id).map((p) => p.id);
|
|
13859
|
+
return {
|
|
13860
|
+
projectIds: new Set(ids),
|
|
13861
|
+
name: program.name,
|
|
13862
|
+
found: true
|
|
13863
|
+
};
|
|
13864
|
+
}
|
|
13865
|
+
const portfolio = byId(data.portfolios).get(scopeId);
|
|
13866
|
+
if (portfolio) {
|
|
13867
|
+
const programIds = new Set(data.programs.filter((p) => p.portfolioId === portfolio.id).map((p) => p.id));
|
|
13868
|
+
const ids = data.projects.filter((p) => programIds.has(p.programId)).map((p) => p.id);
|
|
13869
|
+
return {
|
|
13870
|
+
projectIds: new Set(ids),
|
|
13871
|
+
name: portfolio.name,
|
|
13872
|
+
found: true
|
|
13873
|
+
};
|
|
13874
|
+
}
|
|
13875
|
+
return {
|
|
13876
|
+
projectIds: /* @__PURE__ */ new Set(),
|
|
13877
|
+
name: "Deleted",
|
|
13878
|
+
found: false
|
|
13879
|
+
};
|
|
13880
|
+
}
|
|
13881
|
+
/**
|
|
13882
|
+
* What a chart shows today: its slices in a fixed order (statuses and
|
|
13883
|
+
* priorities in board order, weeks in time order, the rest largest first,
|
|
13884
|
+
* past MAX_BARS summed into "Everything else"), with their total.
|
|
13885
|
+
*/
|
|
13886
|
+
function chartData(chart, data, today) {
|
|
13887
|
+
const scope = scopeOf(data, chart.scopeId);
|
|
13888
|
+
const inScope = (t) => !scope.projectIds || scope.projectIds.has(t.projectId);
|
|
13889
|
+
const unit = chart.measure === "hours" ? "hours" : "tasks";
|
|
13890
|
+
const groupBy = groupsFor(chart.measure).includes(chart.groupBy) ? chart.groupBy : "owner";
|
|
13891
|
+
const items = [];
|
|
13892
|
+
for (const task of data.tasks) {
|
|
13893
|
+
if (!inScope(task)) continue;
|
|
13894
|
+
const done = task.status === "done";
|
|
13895
|
+
switch (chart.measure) {
|
|
13896
|
+
case "open":
|
|
13897
|
+
if (!done) items.push({
|
|
13898
|
+
task,
|
|
13899
|
+
amount: 1,
|
|
13900
|
+
day: task.dueDate
|
|
13901
|
+
});
|
|
13902
|
+
break;
|
|
13903
|
+
case "done":
|
|
13904
|
+
if (done) items.push({
|
|
13905
|
+
task,
|
|
13906
|
+
amount: 1,
|
|
13907
|
+
day: task.completedAt
|
|
13908
|
+
});
|
|
13909
|
+
break;
|
|
13910
|
+
case "all":
|
|
13911
|
+
items.push({
|
|
13912
|
+
task,
|
|
13913
|
+
amount: 1,
|
|
13914
|
+
day: task.createdAt
|
|
13915
|
+
});
|
|
13916
|
+
break;
|
|
13917
|
+
case "overdue":
|
|
13918
|
+
if (!done && task.dueDate && task.dueDate.slice(0, 10) < today) items.push({
|
|
13919
|
+
task,
|
|
13920
|
+
amount: 1,
|
|
13921
|
+
day: task.dueDate
|
|
13922
|
+
});
|
|
13923
|
+
break;
|
|
13924
|
+
case "hours": for (const entry of task.time ?? []) items.push({
|
|
13925
|
+
task,
|
|
13926
|
+
amount: entry.minutes / 60,
|
|
13927
|
+
day: entry.on,
|
|
13928
|
+
personId: entry.personId
|
|
13929
|
+
});
|
|
13930
|
+
}
|
|
13931
|
+
}
|
|
13932
|
+
const projects = byId(data.projects);
|
|
13933
|
+
const programs = byId(data.programs);
|
|
13934
|
+
const people = byId(data.people);
|
|
13935
|
+
const sums = /* @__PURE__ */ new Map();
|
|
13936
|
+
const add = (key, amount) => sums.set(key, (sums.get(key) ?? 0) + amount);
|
|
13937
|
+
let weeks = [];
|
|
13938
|
+
if (groupBy === "week") {
|
|
13939
|
+
const thisWeek = mondayOf(today);
|
|
13940
|
+
const ahead = chart.measure === "open";
|
|
13941
|
+
const first = ahead ? thisWeek : format(addDays(parseIso(`${thisWeek}T12:00:00`), -77), "yyyy-MM-dd");
|
|
13942
|
+
const count = ahead ? 8 : 12;
|
|
13943
|
+
const start = parseIso(`${first}T12:00:00`);
|
|
13944
|
+
weeks = Array.from({ length: count }, (_, i) => format(addDays(start, 7 * i), "yyyy-MM-dd"));
|
|
13945
|
+
const known = new Set(weeks);
|
|
13946
|
+
for (const item of items) {
|
|
13947
|
+
if (!item.day) continue;
|
|
13948
|
+
const week = mondayOf(item.day);
|
|
13949
|
+
if (known.has(week)) add(week, item.amount);
|
|
13950
|
+
else if (ahead && week < first) add(first, item.amount);
|
|
13951
|
+
}
|
|
13952
|
+
} else for (const item of items) {
|
|
13953
|
+
const t = item.task;
|
|
13954
|
+
add(groupBy === "status" ? t.status : groupBy === "priority" ? t.priority : groupBy === "project" ? t.projectId : groupBy === "program" ? projects.get(t.projectId)?.programId ?? "" : (chart.measure === "hours" ? item.personId : t.assigneeId) ?? "", item.amount);
|
|
13955
|
+
}
|
|
13956
|
+
const round = (n) => unit === "hours" ? Math.round(n * 10) / 10 : n;
|
|
13957
|
+
let slices;
|
|
13958
|
+
if (groupBy === "week") slices = weeks.map((w) => ({
|
|
13959
|
+
key: w,
|
|
13960
|
+
label: format(parseIso(`${w}T12:00:00`), "MMM d"),
|
|
13961
|
+
value: round(sums.get(w) ?? 0),
|
|
13962
|
+
fill: "var(--color-primary)"
|
|
13963
|
+
}));
|
|
13964
|
+
else if (groupBy === "status") slices = TASK_COLUMNS.filter((s) => sums.has(s)).map((s) => ({
|
|
13965
|
+
key: s,
|
|
13966
|
+
label: TASK_STATUS_LABEL[s],
|
|
13967
|
+
value: round(sums.get(s)),
|
|
13968
|
+
fill: STATUS_FILL[s]
|
|
13969
|
+
}));
|
|
13970
|
+
else if (groupBy === "priority") slices = PRIORITIES$1.filter((p) => sums.has(p)).map((p) => ({
|
|
13971
|
+
key: p,
|
|
13972
|
+
label: PRIORITY_LABEL[p],
|
|
13973
|
+
value: round(sums.get(p)),
|
|
13974
|
+
fill: PRIORITY_FILL[p]
|
|
13975
|
+
}));
|
|
13976
|
+
else {
|
|
13977
|
+
const named = [...sums.entries()].map(([key, value], i) => {
|
|
13978
|
+
if (groupBy === "owner") {
|
|
13979
|
+
const person = key ? people.get(key) : void 0;
|
|
13980
|
+
return {
|
|
13981
|
+
key: key || "nobody",
|
|
13982
|
+
label: key ? person?.name ?? "Former member" : "Nobody",
|
|
13983
|
+
value: round(value),
|
|
13984
|
+
fill: key ? swatchFill(CYCLE[i % CYCLE.length]) : "var(--color-swatch-slate)"
|
|
13985
|
+
};
|
|
13986
|
+
}
|
|
13987
|
+
const thing = groupBy === "project" ? projects.get(key) : programs.get(key);
|
|
13988
|
+
return {
|
|
13989
|
+
key: key || "none",
|
|
13990
|
+
label: thing?.name ?? "Deleted",
|
|
13991
|
+
value: round(value),
|
|
13992
|
+
fill: thing?.swatch ? swatchFill(thing.swatch) : swatchFill(CYCLE[i % CYCLE.length])
|
|
13993
|
+
};
|
|
13994
|
+
});
|
|
13995
|
+
named.sort((a, b) => b.value - a.value || a.label.localeCompare(b.label));
|
|
13996
|
+
if (named.length > 12) {
|
|
13997
|
+
const rest = named.slice(11);
|
|
13998
|
+
slices = [...named.slice(0, 11), {
|
|
13999
|
+
key: "rest",
|
|
14000
|
+
label: `Everything else (${rest.length})`,
|
|
14001
|
+
value: round(rest.reduce((n, s) => n + s.value, 0)),
|
|
14002
|
+
fill: "var(--color-swatch-slate)"
|
|
14003
|
+
}];
|
|
14004
|
+
} else slices = named;
|
|
14005
|
+
}
|
|
14006
|
+
const total = round(slices.reduce((n, s) => n + s.value, 0));
|
|
14007
|
+
return {
|
|
14008
|
+
slices,
|
|
14009
|
+
total,
|
|
14010
|
+
unit,
|
|
14011
|
+
scopeName: scope.name,
|
|
14012
|
+
scopeFound: scope.found
|
|
14013
|
+
};
|
|
14014
|
+
}
|
|
14015
|
+
/** "12 tasks", "1 task", "7.5 hours". */
|
|
14016
|
+
function amountText(value, unit) {
|
|
14017
|
+
if (unit === "hours") return `${value} ${value === 1 ? "hour" : "hours"}`;
|
|
14018
|
+
return `${value} ${value === 1 ? "task" : "tasks"}`;
|
|
14019
|
+
}
|
|
14020
|
+
/** A chart's text for screen readers, agents and the terminal: "Open tasks by owner: Sam 4, Ana 2 (6 tasks)". */
|
|
14021
|
+
function chartText(title, result) {
|
|
14022
|
+
if (result.slices.length === 0) return `${title}: nothing yet`;
|
|
14023
|
+
return `${title}: ${result.slices.map((s) => `${s.label} ${s.value}`).join(", ")} (${amountText(result.total, result.unit)})`;
|
|
14024
|
+
}
|
|
14025
|
+
|
|
13762
14026
|
// node_modules/zod/v4/core/util.js
|
|
13763
14027
|
function getEnumValues(entries) {
|
|
13764
14028
|
const numericValues = Object.values(entries).filter((v) => typeof v === "number");
|
|
@@ -18956,7 +19220,7 @@ init_blockers();
|
|
|
18956
19220
|
init_format();
|
|
18957
19221
|
init_indexes();
|
|
18958
19222
|
const noon = (iso) => /* @__PURE__ */ new Date(`${iso.slice(0, 10)}T12:00:00`);
|
|
18959
|
-
const days$
|
|
19223
|
+
const days$2 = (from, to) => differenceInCalendarDays(noon(to), noon(from));
|
|
18960
19224
|
const later = (a, b) => !a ? b : !b ? a : a >= b ? a : b;
|
|
18961
19225
|
const open = (t) => t.status !== "done";
|
|
18962
19226
|
/** True when `a` finishes later than `b`, or as late behind a longer line. */
|
|
@@ -19023,7 +19287,7 @@ function linksOf(line, walk, target, today) {
|
|
|
19023
19287
|
task,
|
|
19024
19288
|
finish,
|
|
19025
19289
|
late: Boolean(task.dueDate && task.dueDate.slice(0, 10) < today),
|
|
19026
|
-
slack: against ? days$
|
|
19290
|
+
slack: against ? days$2(finish, against) : null
|
|
19027
19291
|
};
|
|
19028
19292
|
});
|
|
19029
19293
|
}
|
|
@@ -19035,7 +19299,7 @@ function chainOf(line, walk, target, today) {
|
|
|
19035
19299
|
links,
|
|
19036
19300
|
finish,
|
|
19037
19301
|
target,
|
|
19038
|
-
over: target ? days$
|
|
19302
|
+
over: target ? days$2(target, finish) : null,
|
|
19039
19303
|
undated: 0
|
|
19040
19304
|
};
|
|
19041
19305
|
}
|
|
@@ -19116,17 +19380,17 @@ function milestonePole(milestone, data, now) {
|
|
|
19116
19380
|
const others = runs.slice(1).map((r) => ({
|
|
19117
19381
|
project: r.project,
|
|
19118
19382
|
finish: r.finish,
|
|
19119
|
-
slack: days$
|
|
19383
|
+
slack: days$2(r.finish, due)
|
|
19120
19384
|
}));
|
|
19121
19385
|
return {
|
|
19122
19386
|
kind: "chain",
|
|
19123
19387
|
links: first.pole.kind === "chain" ? first.pole.links.map((l, i, all) => i === all.length - 1 ? {
|
|
19124
19388
|
...l,
|
|
19125
|
-
slack: days$
|
|
19389
|
+
slack: days$2(l.finish, due)
|
|
19126
19390
|
} : l) : [],
|
|
19127
19391
|
finish: first.finish,
|
|
19128
19392
|
target: due,
|
|
19129
|
-
over: days$
|
|
19393
|
+
over: days$2(due, first.finish),
|
|
19130
19394
|
via: {
|
|
19131
19395
|
project: first.project,
|
|
19132
19396
|
finish: first.finish
|
|
@@ -19209,7 +19473,8 @@ function pickData(s) {
|
|
|
19209
19473
|
spendEntries: s.spendEntries ?? NONE,
|
|
19210
19474
|
reflections: s.reflections ?? NONE,
|
|
19211
19475
|
priorityRules: s.priorityRules ?? null,
|
|
19212
|
-
workCalendar: s.workCalendar ?? null
|
|
19476
|
+
workCalendar: s.workCalendar ?? null,
|
|
19477
|
+
dashboards: s.dashboards ?? NONE
|
|
19213
19478
|
};
|
|
19214
19479
|
}
|
|
19215
19480
|
/** The document as sent to the server: normalized and without inline file data. */
|
|
@@ -21435,6 +21700,67 @@ const HARBOR_TOOLS = [
|
|
|
21435
21700
|
})) };
|
|
21436
21701
|
}
|
|
21437
21702
|
}),
|
|
21703
|
+
tool({
|
|
21704
|
+
name: "list_dashboards",
|
|
21705
|
+
title: "Dashboards",
|
|
21706
|
+
description: "The harbor's dashboards from the Reports page, each chart with what it counts (open, finished, overdue or all tasks, or hours logged), how it's split (status, owner, priority, project, program or week), the part of the harbor it reads, and its numbers as of today, with a one-line text of each." + DATA_NOTE,
|
|
21707
|
+
input: object({
|
|
21708
|
+
harbor: harborArg,
|
|
21709
|
+
dashboard: string().max(200).optional().describe("Dashboard id or name. Default: all.")
|
|
21710
|
+
}),
|
|
21711
|
+
run: (ctx, input) => {
|
|
21712
|
+
const { data } = ctx;
|
|
21713
|
+
const today = todayISO(ctx.now);
|
|
21714
|
+
const all = data.dashboards ?? [];
|
|
21715
|
+
return { dashboards: (input.dashboard ? [findOne(all, input.dashboard, "dashboard")] : all).map((d) => ({
|
|
21716
|
+
id: d.id,
|
|
21717
|
+
name: d.name,
|
|
21718
|
+
charts: d.charts.map((c) => {
|
|
21719
|
+
const result = chartData(c, data, today);
|
|
21720
|
+
return {
|
|
21721
|
+
id: c.id,
|
|
21722
|
+
title: c.title,
|
|
21723
|
+
counts: MEASURE_LABEL[c.measure],
|
|
21724
|
+
splitBy: GROUP_LABEL[c.groupBy],
|
|
21725
|
+
view: c.view,
|
|
21726
|
+
from: result.scopeName,
|
|
21727
|
+
total: amountText(result.total, result.unit),
|
|
21728
|
+
values: result.slices.map((s) => ({
|
|
21729
|
+
label: s.label,
|
|
21730
|
+
value: s.value
|
|
21731
|
+
})),
|
|
21732
|
+
text: chartText(c.title, result)
|
|
21733
|
+
};
|
|
21734
|
+
})
|
|
21735
|
+
})) };
|
|
21736
|
+
}
|
|
21737
|
+
}),
|
|
21738
|
+
tool({
|
|
21739
|
+
name: "list_task_templates",
|
|
21740
|
+
title: "Task templates",
|
|
21741
|
+
description: "A project's task templates (or every project's), to start new tasks from with create_task's template: each one's name, the title and notes it gives, its priority, owner, checklist and how many days after it's made the task is due." + DATA_NOTE,
|
|
21742
|
+
input: object({
|
|
21743
|
+
harbor: harborArg,
|
|
21744
|
+
project: string().max(200).optional().describe("Project id or name. Default: all.")
|
|
21745
|
+
}),
|
|
21746
|
+
run: (ctx, input) => {
|
|
21747
|
+
const { data } = ctx;
|
|
21748
|
+
const projects = input.project ? [findOne(data.projects, input.project, "project")] : data.projects.filter((p) => p.taskTemplates?.length);
|
|
21749
|
+
const name = (id) => id ? data.people.find((p) => p.id === id)?.name ?? null : null;
|
|
21750
|
+
return { templates: projects.flatMap((p) => (p.taskTemplates ?? []).map((t) => ({
|
|
21751
|
+
id: t.id,
|
|
21752
|
+
name: t.name,
|
|
21753
|
+
project: p.name,
|
|
21754
|
+
title: t.title,
|
|
21755
|
+
notes: t.description || void 0,
|
|
21756
|
+
priority: t.priority,
|
|
21757
|
+
owner: name(t.assigneeId),
|
|
21758
|
+
alsoOn: (t.followerIds ?? []).map(name).filter(Boolean),
|
|
21759
|
+
checklist: t.checklist,
|
|
21760
|
+
dueInDays: t.dueInDays
|
|
21761
|
+
}))) };
|
|
21762
|
+
}
|
|
21763
|
+
}),
|
|
21438
21764
|
tool({
|
|
21439
21765
|
name: "long_pole",
|
|
21440
21766
|
title: "What's holding this up",
|
|
@@ -22246,7 +22572,8 @@ var init_undo = __esmMin((() => {
|
|
|
22246
22572
|
"attachments",
|
|
22247
22573
|
"checkIns",
|
|
22248
22574
|
"spendEntries",
|
|
22249
|
-
"reflections"
|
|
22575
|
+
"reflections",
|
|
22576
|
+
"dashboards"
|
|
22250
22577
|
];
|
|
22251
22578
|
FOLD_MS = 5e3;
|
|
22252
22579
|
LEAD = [
|
|
@@ -22613,6 +22940,9 @@ function hierarchySlice({ get, write }) {
|
|
|
22613
22940
|
...patch
|
|
22614
22941
|
} : p) });
|
|
22615
22942
|
},
|
|
22943
|
+
setDashboards: (dashboards) => {
|
|
22944
|
+
write({ dashboards });
|
|
22945
|
+
},
|
|
22616
22946
|
updateProgram: (id, patch) => {
|
|
22617
22947
|
write({ programs: get().programs.map((p) => p.id === id ? {
|
|
22618
22948
|
...p,
|
|
@@ -22942,6 +23272,16 @@ var init_github_refs = __esmMin((() => {
|
|
|
22942
23272
|
}));
|
|
22943
23273
|
|
|
22944
23274
|
// src/lib/automations.ts
|
|
23275
|
+
/** Whether a task meets the rule's "only if" line. */
|
|
23276
|
+
function meetsCondition(rule, task) {
|
|
23277
|
+
const c = rule.onlyIf;
|
|
23278
|
+
if (!c) return true;
|
|
23279
|
+
if (c.priority && task.priority !== c.priority) return false;
|
|
23280
|
+
if (c.assignee) {
|
|
23281
|
+
if (c.assignee === "nobody" ? task.assigneeId != null : task.assigneeId !== c.assignee) return false;
|
|
23282
|
+
}
|
|
23283
|
+
return true;
|
|
23284
|
+
}
|
|
22945
23285
|
/**
|
|
22946
23286
|
* Checked once, against the (before, after) pair of a single user-driven
|
|
22947
23287
|
* mutation — never re-checked against an automation's own effect. That's the
|
|
@@ -22959,6 +23299,64 @@ function matches(rule, prev, next, created) {
|
|
|
22959
23299
|
default: return false;
|
|
22960
23300
|
}
|
|
22961
23301
|
}
|
|
23302
|
+
/** One rule's actions on one task, in order. */
|
|
23303
|
+
function runActions(actions, start, ctx) {
|
|
23304
|
+
let task = start;
|
|
23305
|
+
const comments = [];
|
|
23306
|
+
for (const action of actions) switch (action.type) {
|
|
23307
|
+
case "set_status": {
|
|
23308
|
+
if (!action.status) break;
|
|
23309
|
+
const wasDone = task.status === "done";
|
|
23310
|
+
const completedAt = action.status === "done" ? task.completedAt ?? ctx.now : wasDone ? null : task.completedAt;
|
|
23311
|
+
task = {
|
|
23312
|
+
...task,
|
|
23313
|
+
status: action.status,
|
|
23314
|
+
completedAt
|
|
23315
|
+
};
|
|
23316
|
+
break;
|
|
23317
|
+
}
|
|
23318
|
+
case "set_priority":
|
|
23319
|
+
if (action.priority) task = {
|
|
23320
|
+
...task,
|
|
23321
|
+
priority: action.priority
|
|
23322
|
+
};
|
|
23323
|
+
break;
|
|
23324
|
+
case "set_assignee":
|
|
23325
|
+
task = {
|
|
23326
|
+
...task,
|
|
23327
|
+
assigneeId: action.assigneeId ?? null
|
|
23328
|
+
};
|
|
23329
|
+
break;
|
|
23330
|
+
case "set_due": {
|
|
23331
|
+
const n = Math.min(365, Math.max(0, Math.round(action.inDays ?? 0)));
|
|
23332
|
+
task = {
|
|
23333
|
+
...task,
|
|
23334
|
+
dueDate: dayAfter(ctx.today, n)
|
|
23335
|
+
};
|
|
23336
|
+
break;
|
|
23337
|
+
}
|
|
23338
|
+
case "add_follower": {
|
|
23339
|
+
const id = action.personId;
|
|
23340
|
+
if (!id || id === task.assigneeId || task.followerIds?.includes(id)) break;
|
|
23341
|
+
task = {
|
|
23342
|
+
...task,
|
|
23343
|
+
followerIds: [...task.followerIds ?? [], id]
|
|
23344
|
+
};
|
|
23345
|
+
break;
|
|
23346
|
+
}
|
|
23347
|
+
case "add_comment": if (action.commentBody?.trim()) comments.push({
|
|
23348
|
+
taskId: task.id,
|
|
23349
|
+
authorId: ctx.authorId,
|
|
23350
|
+
body: action.commentBody.trim(),
|
|
23351
|
+
createdAt: ctx.now,
|
|
23352
|
+
mentionedIds: []
|
|
23353
|
+
});
|
|
23354
|
+
}
|
|
23355
|
+
return {
|
|
23356
|
+
task,
|
|
23357
|
+
comments
|
|
23358
|
+
};
|
|
23359
|
+
}
|
|
22962
23360
|
/**
|
|
22963
23361
|
* Applies every enabled rule on `project` whose trigger matches this one
|
|
22964
23362
|
* mutation (`prev` -> `next`, or `prev` undefined for a brand-new task) and
|
|
@@ -22969,49 +23367,32 @@ function matches(rule, prev, next, created) {
|
|
|
22969
23367
|
function applyAutomations(project, prev, next, opts) {
|
|
22970
23368
|
const rules = project?.automations ?? [];
|
|
22971
23369
|
const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
23370
|
+
const today = opts.now ? opts.now.slice(0, 10) : todayISO();
|
|
23371
|
+
const ctx = {
|
|
23372
|
+
authorId: opts.currentUserId,
|
|
23373
|
+
now,
|
|
23374
|
+
today
|
|
23375
|
+
};
|
|
22972
23376
|
let task = next;
|
|
22973
23377
|
const comments = [];
|
|
22974
23378
|
for (const rule of rules) {
|
|
22975
|
-
if (!matches(rule, prev, next, opts.created)) continue;
|
|
22976
|
-
|
|
22977
|
-
|
|
22978
|
-
|
|
22979
|
-
const wasDone = task.status === "done";
|
|
22980
|
-
const completedAt = action.status === "done" ? task.completedAt ?? now : wasDone ? null : task.completedAt;
|
|
22981
|
-
task = {
|
|
22982
|
-
...task,
|
|
22983
|
-
status: action.status,
|
|
22984
|
-
completedAt
|
|
22985
|
-
};
|
|
22986
|
-
break;
|
|
22987
|
-
}
|
|
22988
|
-
case "set_priority":
|
|
22989
|
-
if (action.priority) task = {
|
|
22990
|
-
...task,
|
|
22991
|
-
priority: action.priority
|
|
22992
|
-
};
|
|
22993
|
-
break;
|
|
22994
|
-
case "set_assignee":
|
|
22995
|
-
task = {
|
|
22996
|
-
...task,
|
|
22997
|
-
assigneeId: action.assigneeId ?? null
|
|
22998
|
-
};
|
|
22999
|
-
break;
|
|
23000
|
-
case "add_comment": if (action.commentBody?.trim()) comments.push({
|
|
23001
|
-
taskId: task.id,
|
|
23002
|
-
authorId: opts.currentUserId,
|
|
23003
|
-
body: action.commentBody.trim(),
|
|
23004
|
-
createdAt: now,
|
|
23005
|
-
mentionedIds: []
|
|
23006
|
-
});
|
|
23007
|
-
}
|
|
23379
|
+
if (!matches(rule, prev, next, opts.created) || !meetsCondition(rule, next)) continue;
|
|
23380
|
+
const ran = runActions(rule.actions, task, ctx);
|
|
23381
|
+
task = ran.task;
|
|
23382
|
+
comments.push(...ran.comments);
|
|
23008
23383
|
}
|
|
23009
23384
|
return {
|
|
23010
23385
|
task,
|
|
23011
23386
|
comments
|
|
23012
23387
|
};
|
|
23013
23388
|
}
|
|
23014
|
-
var
|
|
23389
|
+
var dayAfter;
|
|
23390
|
+
var init_automations = __esmMin((() => {
|
|
23391
|
+
init_date_fns();
|
|
23392
|
+
init_date_text();
|
|
23393
|
+
init_format();
|
|
23394
|
+
dayAfter = (day, n) => format(addDays(parseIso(day), n), "yyyy-MM-dd");
|
|
23395
|
+
}));
|
|
23015
23396
|
|
|
23016
23397
|
// src/lib/focus.ts
|
|
23017
23398
|
function focusIdsOf(data, personId, date = todayISO()) {
|
|
@@ -24499,7 +24880,7 @@ function pickCollections(doc, keys) {
|
|
|
24499
24880
|
function emptyRows() {
|
|
24500
24881
|
return Object.fromEntries(ROW_COLLECTIONS.map((k) => [k, []]));
|
|
24501
24882
|
}
|
|
24502
|
-
/** A collection as rows; `spendEntries`, `reflections`, `priorityRules` and `
|
|
24883
|
+
/** A collection as rows; `spendEntries`, `reflections`, `priorityRules`, `workCalendar` and `dashboards` may be missing from older documents. */
|
|
24503
24884
|
function rowsOf(doc, key) {
|
|
24504
24885
|
if (isKeyed(key)) return KEYED[key].rows(doc[key]);
|
|
24505
24886
|
return doc[key] ?? [];
|
|
@@ -24584,7 +24965,8 @@ var init_changes = __esmMin((() => {
|
|
|
24584
24965
|
"spendEntries",
|
|
24585
24966
|
"reflections",
|
|
24586
24967
|
"priorityRules",
|
|
24587
|
-
"workCalendar"
|
|
24968
|
+
"workCalendar",
|
|
24969
|
+
"dashboards"
|
|
24588
24970
|
];
|
|
24589
24971
|
ROW_COLLECTIONS = [
|
|
24590
24972
|
"tasks",
|
|
@@ -24605,7 +24987,8 @@ var init_changes = __esmMin((() => {
|
|
|
24605
24987
|
"ropes",
|
|
24606
24988
|
"readPingIds",
|
|
24607
24989
|
"priorityRules",
|
|
24608
|
-
"workCalendar"
|
|
24990
|
+
"workCalendar",
|
|
24991
|
+
"dashboards"
|
|
24609
24992
|
];
|
|
24610
24993
|
KEYED = {
|
|
24611
24994
|
boardConfigs: {
|
|
@@ -28404,7 +28787,7 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
28404
28787
|
if (input === "p" && mode !== "budget") return go.pick("Show cards of priority", [{
|
|
28405
28788
|
label: "Any priority",
|
|
28406
28789
|
value: "any"
|
|
28407
|
-
}, ...PRIORITIES$
|
|
28790
|
+
}, ...PRIORITIES$2.map((p) => ({
|
|
28408
28791
|
label: PRIORITY_LABEL[p],
|
|
28409
28792
|
value: p
|
|
28410
28793
|
}))], (priority) => setFilter((f) => ({
|
|
@@ -29024,13 +29407,18 @@ const TRIGGER_LABEL = {
|
|
|
29024
29407
|
task_created: "A task is created",
|
|
29025
29408
|
status_changed_to: "Status is set to…",
|
|
29026
29409
|
priority_changed_to: "Priority is set to…",
|
|
29027
|
-
assigned: "A task is assigned to anyone"
|
|
29410
|
+
assigned: "A task is assigned to anyone",
|
|
29411
|
+
due_soon: "A task is due soon",
|
|
29412
|
+
overdue: "A task becomes overdue",
|
|
29413
|
+
untouched: "A task sits untouched"
|
|
29028
29414
|
};
|
|
29029
29415
|
const ACTION_LABEL = {
|
|
29030
29416
|
set_status: "Set status to…",
|
|
29031
29417
|
set_priority: "Set priority to…",
|
|
29032
29418
|
set_assignee: "Assign to…",
|
|
29033
|
-
add_comment: "Post a comment"
|
|
29419
|
+
add_comment: "Post a comment",
|
|
29420
|
+
set_due: "Move the due date",
|
|
29421
|
+
add_follower: "Add a follower"
|
|
29034
29422
|
};
|
|
29035
29423
|
function describeAction(a, peopleName) {
|
|
29036
29424
|
switch (a.type) {
|
|
@@ -29038,18 +29426,36 @@ function describeAction(a, peopleName) {
|
|
|
29038
29426
|
case "set_priority": return `set priority to ${a.priority ? PRIORITY_LABEL[a.priority] : "…"}`;
|
|
29039
29427
|
case "set_assignee": return `assign to ${peopleName(a.assigneeId)}`;
|
|
29040
29428
|
case "add_comment": return `post “${a.commentBody ?? ""}”`;
|
|
29429
|
+
case "set_due": {
|
|
29430
|
+
const n = a.inDays ?? 0;
|
|
29431
|
+
return n === 0 ? "set the due date to that day" : `set the due date ${n} ${n === 1 ? "day" : "days"} out`;
|
|
29432
|
+
}
|
|
29433
|
+
case "add_follower": return `add ${peopleName(a.personId)} as a follower`;
|
|
29041
29434
|
}
|
|
29042
29435
|
}
|
|
29436
|
+
const days$1 = (n) => `${n} ${n === 1 ? "day" : "days"}`;
|
|
29043
29437
|
function describeTrigger(rule) {
|
|
29044
29438
|
const t = rule.trigger;
|
|
29045
29439
|
if (t.type === "status_changed_to") return `When status is set to ${t.status ? TASK_STATUS_LABEL[t.status] : "…"}`;
|
|
29046
29440
|
if (t.type === "priority_changed_to") return `When priority is set to ${t.priority ? PRIORITY_LABEL[t.priority] : "…"}`;
|
|
29441
|
+
if (t.type === "due_soon") return `When a task is due within ${days$1(t.days ?? 2)}`;
|
|
29442
|
+
if (t.type === "untouched") return `When nobody touches a task for ${days$1(t.days ?? 7)}`;
|
|
29047
29443
|
return `When ${TRIGGER_LABEL[t.type].toLowerCase()}`;
|
|
29048
29444
|
}
|
|
29445
|
+
/** The "only if" line: "only High priority, owned by Ana", or "" when there's none. */
|
|
29446
|
+
function describeCondition(rule, peopleName) {
|
|
29447
|
+
const c = rule.onlyIf;
|
|
29448
|
+
if (!c) return "";
|
|
29449
|
+
const parts = [];
|
|
29450
|
+
if (c.priority) parts.push(`${PRIORITY_LABEL[c.priority]} priority`);
|
|
29451
|
+
if (c.assignee) parts.push(c.assignee === "nobody" ? "with no owner" : `owned by ${peopleName(c.assignee)}`);
|
|
29452
|
+
return parts.length ? `only ${parts.join(", ")}` : "";
|
|
29453
|
+
}
|
|
29049
29454
|
/** "When status is set to Done: set priority to Low, post “Shipped”". */
|
|
29050
29455
|
function describeRule(rule, peopleName) {
|
|
29051
29456
|
const did = rule.actions.map((a) => describeAction(a, peopleName)).join(", ");
|
|
29052
|
-
|
|
29457
|
+
const only = describeCondition(rule, peopleName);
|
|
29458
|
+
return `${describeTrigger(rule)}${only ? ` (${only})` : ""}: ${did || "no actions"}`;
|
|
29053
29459
|
}
|
|
29054
29460
|
function defaultAction(type) {
|
|
29055
29461
|
if (type === "set_status") return {
|
|
@@ -29064,6 +29470,14 @@ function defaultAction(type) {
|
|
|
29064
29470
|
type,
|
|
29065
29471
|
assigneeId: null
|
|
29066
29472
|
};
|
|
29473
|
+
if (type === "set_due") return {
|
|
29474
|
+
type,
|
|
29475
|
+
inDays: 3
|
|
29476
|
+
};
|
|
29477
|
+
if (type === "add_follower") return {
|
|
29478
|
+
type,
|
|
29479
|
+
personId: null
|
|
29480
|
+
};
|
|
29067
29481
|
return {
|
|
29068
29482
|
type,
|
|
29069
29483
|
commentBody: ""
|
|
@@ -29103,6 +29517,7 @@ function readFieldValue(def, text) {
|
|
|
29103
29517
|
* starts from, each column (name, shown, cap, place), lanes and what cards
|
|
29104
29518
|
* show, the project's own fields and its automations. b on a project opens it.
|
|
29105
29519
|
*/
|
|
29520
|
+
init_automations();
|
|
29106
29521
|
init_board();
|
|
29107
29522
|
init_swatches();
|
|
29108
29523
|
const LANES = [
|
|
@@ -29137,7 +29552,7 @@ const statusChoices = () => TASK_COLUMNS.map((s) => ({
|
|
|
29137
29552
|
label: TASK_STATUS_LABEL[s],
|
|
29138
29553
|
value: s
|
|
29139
29554
|
}));
|
|
29140
|
-
const priorityChoices = () => PRIORITIES$
|
|
29555
|
+
const priorityChoices = () => PRIORITIES$2.map((p) => ({
|
|
29141
29556
|
label: PRIORITY_LABEL[p],
|
|
29142
29557
|
value: p
|
|
29143
29558
|
}));
|
|
@@ -29391,6 +29806,14 @@ function BoardPanel({ id, width, height, go, active, onClose }) {
|
|
|
29391
29806
|
type,
|
|
29392
29807
|
priority: p
|
|
29393
29808
|
}));
|
|
29809
|
+
if (type === "due_soon" || type === "untouched") return go.ask(type === "due_soon" ? "Due within how many days?" : "Untouched for how many days?", (text) => {
|
|
29810
|
+
const n = Math.round(Number(text.trim()));
|
|
29811
|
+
if (!Number.isFinite(n) || n < 1) return;
|
|
29812
|
+
askAction({
|
|
29813
|
+
type,
|
|
29814
|
+
days: Math.min(60, n)
|
|
29815
|
+
});
|
|
29816
|
+
});
|
|
29394
29817
|
askAction({ type });
|
|
29395
29818
|
}
|
|
29396
29819
|
function askAction(trigger) {
|
|
@@ -29424,6 +29847,21 @@ function BoardPanel({ id, width, height, go, active, onClose }) {
|
|
|
29424
29847
|
...defaultAction(type),
|
|
29425
29848
|
assigneeId: pid
|
|
29426
29849
|
}));
|
|
29850
|
+
if (type === "set_due") return go.ask("Set the due date how many days out?", (text) => {
|
|
29851
|
+
const n = Math.round(Number(text.trim()));
|
|
29852
|
+
if (Number.isFinite(n) && n >= 0) add({
|
|
29853
|
+
type,
|
|
29854
|
+
inDays: Math.min(365, n)
|
|
29855
|
+
});
|
|
29856
|
+
});
|
|
29857
|
+
if (type === "add_follower") return go.pick("Add as a follower", people.map((p) => ({
|
|
29858
|
+
label: p.name,
|
|
29859
|
+
value: p.id,
|
|
29860
|
+
hint: p.role
|
|
29861
|
+
})), (pid) => add({
|
|
29862
|
+
type,
|
|
29863
|
+
personId: pid
|
|
29864
|
+
}));
|
|
29427
29865
|
go.ask("Comment to post", (text) => text.trim() && add({
|
|
29428
29866
|
type,
|
|
29429
29867
|
commentBody: text.trim()
|
|
@@ -29822,11 +30260,11 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
|
|
|
29822
30260
|
});
|
|
29823
30261
|
} else if (f === "priority") editor = /* @__PURE__ */ jsx(Picker, {
|
|
29824
30262
|
title: "Priority",
|
|
29825
|
-
choices: PRIORITIES$
|
|
30263
|
+
choices: PRIORITIES$2.map((p) => ({
|
|
29826
30264
|
label: PRIORITY_LABEL[p],
|
|
29827
30265
|
value: p
|
|
29828
30266
|
})),
|
|
29829
|
-
initial: PRIORITIES$
|
|
30267
|
+
initial: PRIORITIES$2.indexOf(task.priority),
|
|
29830
30268
|
onCancel: done,
|
|
29831
30269
|
onPick: (p) => {
|
|
29832
30270
|
store.getState().updateTask(task.id, { priority: p });
|
package/lib/cli.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
followedText,
|
|
41
41
|
soundingsText,
|
|
42
42
|
goalsText,
|
|
43
|
+
templatesText,
|
|
43
44
|
thanksText,
|
|
44
45
|
ashoreText,
|
|
45
46
|
voyageText,
|
|
@@ -166,7 +167,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
166
167
|
[--also "NAME, NAME"] [--due DATE|pilot]
|
|
167
168
|
[--priority low|medium|high|urgent]
|
|
168
169
|
[--status S] [--comment TEXT] [--over-limit] [--dry-run]
|
|
169
|
-
[--literal]
|
|
170
|
+
[--literal] [--template NAME]
|
|
170
171
|
reads plain words in TITLE as the
|
|
171
172
|
website does: @name, by friday,
|
|
172
173
|
!high, #project (--literal: keep
|
|
@@ -263,6 +264,10 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
263
264
|
the ones waiting on your answer
|
|
264
265
|
harbor goals [PORTFOLIO] each portfolio's goals, how far
|
|
265
266
|
along and whether on track
|
|
267
|
+
harbor templates [PROJECT] the task templates new tasks
|
|
268
|
+
start from (harbor add --template)
|
|
269
|
+
harbor template TASK [--name NAME] save a task as a template in its
|
|
270
|
+
project
|
|
266
271
|
harbor ashore [--weeks N] [--country CODES|none] [--person NAME]
|
|
267
272
|
public holidays and time away
|
|
268
273
|
coming up, and whose holidays
|
|
@@ -768,6 +773,11 @@ const DETAIL = {
|
|
|
768
773
|
" ten lines with --comment add the same comment to all ten. More than 100",
|
|
769
774
|
" lines is harbor import's job.",
|
|
770
775
|
"",
|
|
776
|
+
" --template NAME starts from one of the project's task templates (save one",
|
|
777
|
+
" with Save as template on a task, or harbor template TASK): its title,",
|
|
778
|
+
" notes, priority, owner, others on it, checklist and due offset fill what",
|
|
779
|
+
" the flags don't. Leave out TITLE to take the template's.",
|
|
780
|
+
"",
|
|
771
781
|
" --due pilot takes the earliest day the owner's queue suggests: their open",
|
|
772
782
|
" work due first, their weekly pace, time away, what it waits on and how",
|
|
773
783
|
" long their similar tasks took. It needs an owner (--assignee none is",
|
|
@@ -1026,6 +1036,16 @@ const DETAIL = {
|
|
|
1026
1036
|
" these as days nobody works. Countries: AU BR CA DE ES FR GB IE IT MX NL",
|
|
1027
1037
|
" NZ US (national holidays only).",
|
|
1028
1038
|
],
|
|
1039
|
+
templates: [
|
|
1040
|
+
" harbor templates [PROJECT] reads: each project's task templates",
|
|
1041
|
+
" (all projects when left out)",
|
|
1042
|
+
" harbor template TASK [--name NAME] writes: saves the task as a template",
|
|
1043
|
+
" in its project, named after it unless",
|
|
1044
|
+
" --name says otherwise",
|
|
1045
|
+
"",
|
|
1046
|
+
" harbor add --project P --template NAME starts a task from one: its title,",
|
|
1047
|
+
" notes, priority, owner, others on it, checklist and due offset.",
|
|
1048
|
+
],
|
|
1029
1049
|
goals: [
|
|
1030
1050
|
" harbor goals [PORTFOLIO] reads: each portfolio's goals (all when",
|
|
1031
1051
|
" left out), off track first, with how far",
|
|
@@ -2478,12 +2498,15 @@ export async function run(argv, io = {}) {
|
|
|
2478
2498
|
const { positionals, values, harbor } = parse(
|
|
2479
2499
|
spec(
|
|
2480
2500
|
["over-limit", "dry-run", "literal"],
|
|
2481
|
-
["project", "assignee", "also", "due", "priority", "status", "comment"],
|
|
2501
|
+
["project", "assignee", "also", "due", "priority", "status", "comment", "template"],
|
|
2482
2502
|
),
|
|
2483
2503
|
);
|
|
2504
|
+
// --template with no title: the template's own title.
|
|
2505
|
+
const fromTemplate = values.template !== undefined && positionals.length === 0;
|
|
2484
2506
|
// Several at once: one title a line on stdin, with no title (or -) after add.
|
|
2485
2507
|
const piped =
|
|
2486
|
-
|
|
2508
|
+
!fromTemplate &&
|
|
2509
|
+
(positionals.length === 0 || (positionals.length === 1 && positionals[0] === "-"));
|
|
2487
2510
|
const stdin = io.stdin ?? process.stdin;
|
|
2488
2511
|
const titles =
|
|
2489
2512
|
piped && (io.stdin || !process.stdin.isTTY)
|
|
@@ -2491,7 +2514,9 @@ export async function run(argv, io = {}) {
|
|
|
2491
2514
|
.split(/\r?\n/)
|
|
2492
2515
|
.map((l) => l.trim())
|
|
2493
2516
|
.filter(Boolean)
|
|
2494
|
-
:
|
|
2517
|
+
: fromTemplate
|
|
2518
|
+
? [""]
|
|
2519
|
+
: [text(positionals, "the task")];
|
|
2495
2520
|
if (titles.length === 0)
|
|
2496
2521
|
throw new HarborError("Say the task, or give one title a line on stdin.");
|
|
2497
2522
|
if (titles.length > 100)
|
|
@@ -2500,7 +2525,7 @@ export async function run(argv, io = {}) {
|
|
|
2500
2525
|
);
|
|
2501
2526
|
// Plain words, as on the website: @person, by Friday, !high, #Project.
|
|
2502
2527
|
// --literal keeps every title exactly as typed.
|
|
2503
|
-
const reads = values.literal
|
|
2528
|
+
const reads = values.literal || fromTemplate
|
|
2504
2529
|
? titles.map((title) => ({ title, owner: null, due: null, priority: null, project: null }))
|
|
2505
2530
|
: await readTitles(client, harbor, titles, values);
|
|
2506
2531
|
// No --project and no #Project: the harbor's one open project, if that's all there is.
|
|
@@ -2569,9 +2594,10 @@ export async function run(argv, io = {}) {
|
|
|
2569
2594
|
.call("create_task", {
|
|
2570
2595
|
harbor,
|
|
2571
2596
|
project: projectOf(words),
|
|
2572
|
-
title: words.title,
|
|
2573
|
-
|
|
2574
|
-
|
|
2597
|
+
title: words.title || undefined,
|
|
2598
|
+
template: values.template,
|
|
2599
|
+
// Yours unless you say otherwise, as with n in the app (a template keeps its own owner).
|
|
2600
|
+
assignee: values.assignee ?? words.owner?.value ?? (values.template ? undefined : "me"),
|
|
2575
2601
|
also: values.also === undefined ? undefined : alsoList(values.also),
|
|
2576
2602
|
due: duePilot(values.due) ? undefined : (dueDate(values.due) ?? words.due?.value),
|
|
2577
2603
|
suggestedDue: duePilot(values.due),
|
|
@@ -3190,6 +3216,23 @@ export async function run(argv, io = {}) {
|
|
|
3190
3216
|
soundingsText(r),
|
|
3191
3217
|
);
|
|
3192
3218
|
}
|
|
3219
|
+
case "templates": {
|
|
3220
|
+
const { positionals, harbor } = parse(spec());
|
|
3221
|
+
const project = positionals.join(" ").trim();
|
|
3222
|
+
return call("list_task_templates", { harbor, ...(project ? { project } : {}) }, (r) =>
|
|
3223
|
+
templatesText(r),
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
case "template": {
|
|
3227
|
+
const { positionals, values, harbor } = parse(spec([], ["name"]));
|
|
3228
|
+
const task = text(positionals, "the task");
|
|
3229
|
+
return call(
|
|
3230
|
+
"save_task_template",
|
|
3231
|
+
{ harbor, task, ...(values.name ? { name: values.name } : {}) },
|
|
3232
|
+
(r) =>
|
|
3233
|
+
`Saved “${r.template.name}” as a template in ${r.template.project}. harbor add --project "${r.template.project}" --template "${r.template.name}" starts a task from it.`,
|
|
3234
|
+
);
|
|
3235
|
+
}
|
|
3193
3236
|
case "goals": {
|
|
3194
3237
|
const { positionals, harbor } = parse(spec());
|
|
3195
3238
|
const portfolio = positionals.join(" ").trim();
|
package/lib/format.mjs
CHANGED
|
@@ -1231,6 +1231,30 @@ export function catchupText(r) {
|
|
|
1231
1231
|
}
|
|
1232
1232
|
|
|
1233
1233
|
/** list_goals as text: each portfolio's goals, what needs attention first, with their key results. */
|
|
1234
|
+
/** harbor templates: each project's task templates, with what a new task takes from them. */
|
|
1235
|
+
export function templatesText(r) {
|
|
1236
|
+
const list = r.templates ?? [];
|
|
1237
|
+
if (!list.length)
|
|
1238
|
+
return "No task templates yet. harbor template TASK saves one, or Save as template on a task on the website.";
|
|
1239
|
+
const lines = [];
|
|
1240
|
+
let project = null;
|
|
1241
|
+
for (const t of list) {
|
|
1242
|
+
if (t.project !== project) {
|
|
1243
|
+
if (project !== null) lines.push("");
|
|
1244
|
+
lines.push(t.project);
|
|
1245
|
+
project = t.project;
|
|
1246
|
+
}
|
|
1247
|
+
const bits = [];
|
|
1248
|
+
if (t.priority && t.priority !== "medium") bits.push(`${t.priority} priority`);
|
|
1249
|
+
if (t.owner) bits.push(t.owner);
|
|
1250
|
+
if (t.dueInDays != null)
|
|
1251
|
+
bits.push(t.dueInDays === 0 ? "due the day it's made" : `due in ${t.dueInDays} ${t.dueInDays === 1 ? "day" : "days"}`);
|
|
1252
|
+
if (t.checklist?.length) bits.push(`${t.checklist.length} checklist ${t.checklist.length === 1 ? "item" : "items"}`);
|
|
1253
|
+
lines.push(` ${t.name}${t.title !== t.name ? `: ${t.title}` : ""}${bits.length ? ` (${bits.join(", ")})` : ""}`);
|
|
1254
|
+
}
|
|
1255
|
+
return lines.join("\n");
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1234
1258
|
export function goalsText(r) {
|
|
1235
1259
|
const order = ["off-track", "at-risk", "not-started", "on-track", "achieved"];
|
|
1236
1260
|
const withGoals = (r.portfolios ?? []).filter((p) => p.goals?.length);
|