@harborppm/cli 0.11.0 → 0.11.2
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 +48 -27
- package/lib/cli.mjs +7 -6
- package/lib/format.mjs +30 -10
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -3285,6 +3285,19 @@ function weeklyLoad(tasks, personId, now, weeks = 6) {
|
|
|
3285
3285
|
};
|
|
3286
3286
|
}
|
|
3287
3287
|
|
|
3288
|
+
//#endregion
|
|
3289
|
+
//#region src/lib/selectors.ts
|
|
3290
|
+
/** Projects (or programs) still running, and the ones set to Done, which lists tuck away until asked for. */
|
|
3291
|
+
function splitDone(items) {
|
|
3292
|
+
const open = [];
|
|
3293
|
+
const done = [];
|
|
3294
|
+
for (const item of items) (item.health === "done" ? done : open).push(item);
|
|
3295
|
+
return {
|
|
3296
|
+
open,
|
|
3297
|
+
done
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3288
3301
|
//#endregion
|
|
3289
3302
|
//#region src/tui/views/lists.tsx
|
|
3290
3303
|
const count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
@@ -3335,19 +3348,23 @@ function RowList({ title, right, rows, empty, active, width, height, onOpen, key
|
|
|
3335
3348
|
]
|
|
3336
3349
|
});
|
|
3337
3350
|
}
|
|
3338
|
-
/** Every project, under its program and portfolio, with health and progress.
|
|
3351
|
+
/** Every project, under its program and portfolio, with health and progress. Done
|
|
3352
|
+
* projects and programs stay out of the way until D shows them. */
|
|
3339
3353
|
function ProjectsView({ active, width, height, go }) {
|
|
3340
3354
|
const portfolios = useHarbor((s) => s.portfolios);
|
|
3341
3355
|
const programs = useHarbor((s) => s.programs);
|
|
3342
3356
|
const projects = useHarbor((s) => s.projects);
|
|
3343
3357
|
const tasks = useHarbor((s) => s.tasks);
|
|
3344
3358
|
const { today } = useDeps();
|
|
3345
|
-
const
|
|
3359
|
+
const [showDone, setShowDone] = useState(false);
|
|
3360
|
+
const { rows, shown, hidden } = useMemo(() => {
|
|
3346
3361
|
const data = {
|
|
3347
3362
|
tasks,
|
|
3348
3363
|
projects
|
|
3349
3364
|
};
|
|
3350
3365
|
const out = [];
|
|
3366
|
+
let shown = 0;
|
|
3367
|
+
let hidden = 0;
|
|
3351
3368
|
for (const portfolio of portfolios) {
|
|
3352
3369
|
out.push({
|
|
3353
3370
|
id: portfolio.id,
|
|
@@ -3358,6 +3375,11 @@ function ProjectsView({ active, width, height, go }) {
|
|
|
3358
3375
|
health: portfolio.health
|
|
3359
3376
|
});
|
|
3360
3377
|
for (const program of programs.filter((g) => g.portfolioId === portfolio.id)) {
|
|
3378
|
+
const { open, done } = splitDone(projects.filter((p) => p.programId === program.id));
|
|
3379
|
+
if (program.health === "done" && open.length === 0) {
|
|
3380
|
+
hidden += 1 + done.length;
|
|
3381
|
+
if (!showDone) continue;
|
|
3382
|
+
} else hidden += done.length;
|
|
3361
3383
|
out.push({
|
|
3362
3384
|
id: program.id,
|
|
3363
3385
|
kind: "program",
|
|
@@ -3365,8 +3387,9 @@ function ProjectsView({ active, width, height, go }) {
|
|
|
3365
3387
|
title: program.name,
|
|
3366
3388
|
health: program.health
|
|
3367
3389
|
});
|
|
3368
|
-
for (const project of
|
|
3369
|
-
|
|
3390
|
+
for (const project of showDone ? [...open, ...done] : open) {
|
|
3391
|
+
shown++;
|
|
3392
|
+
const { done: finished, total } = projectSummary(data, project);
|
|
3370
3393
|
const late = tasksOf(tasks, project.id).filter((t) => isLate(t, today())).length;
|
|
3371
3394
|
out.push({
|
|
3372
3395
|
id: project.id,
|
|
@@ -3374,22 +3397,27 @@ function ProjectsView({ active, width, height, go }) {
|
|
|
3374
3397
|
depth: 2,
|
|
3375
3398
|
title: `${project.name}${project.key ? ` ${project.key}` : ""}`,
|
|
3376
3399
|
health: project.health,
|
|
3377
|
-
detail: `${
|
|
3400
|
+
detail: `${finished}/${total} done${late ? ` · ${late} late` : ""} · ends ${shortDay(project.endDate, today())}`
|
|
3378
3401
|
});
|
|
3379
3402
|
}
|
|
3380
3403
|
}
|
|
3381
3404
|
}
|
|
3382
|
-
return
|
|
3405
|
+
return {
|
|
3406
|
+
rows: out,
|
|
3407
|
+
shown,
|
|
3408
|
+
hidden
|
|
3409
|
+
};
|
|
3383
3410
|
}, [
|
|
3384
3411
|
portfolios,
|
|
3385
3412
|
programs,
|
|
3386
3413
|
projects,
|
|
3387
3414
|
tasks,
|
|
3388
|
-
today
|
|
3415
|
+
today,
|
|
3416
|
+
showDone
|
|
3389
3417
|
]);
|
|
3390
3418
|
return /* @__PURE__ */ jsx(RowList, {
|
|
3391
3419
|
title: "Portfolios, programs and projects",
|
|
3392
|
-
right: `${count(
|
|
3420
|
+
right: `${count(shown, "project")}${hidden ? ` · ${hidden} done ${showDone ? "shown" : "hidden"} (D)` : ""} · n new project · e edit`,
|
|
3393
3421
|
rows,
|
|
3394
3422
|
empty: "No projects yet. n adds one.",
|
|
3395
3423
|
active,
|
|
@@ -3401,7 +3429,8 @@ function ProjectsView({ active, width, height, go }) {
|
|
|
3401
3429
|
const programId = r?.kind === "program" ? r.id : r?.kind === "project" ? projects.find((p) => p.id === r.id)?.programId : void 0;
|
|
3402
3430
|
go.create("project", programId ? { programId } : {});
|
|
3403
3431
|
},
|
|
3404
|
-
e: (r) => r && r.kind !== "raid" && r.kind !== "person" && go.openPanel(r.kind, r.id)
|
|
3432
|
+
e: (r) => r && r.kind !== "raid" && r.kind !== "person" && go.openPanel(r.kind, r.id),
|
|
3433
|
+
D: () => hidden > 0 && setShowDone((v) => !v)
|
|
3405
3434
|
}
|
|
3406
3435
|
});
|
|
3407
3436
|
}
|
|
@@ -3411,7 +3440,10 @@ function ProgramsView({ active, width, height, go }) {
|
|
|
3411
3440
|
const portfolios = useHarbor((s) => s.portfolios);
|
|
3412
3441
|
const milestones = useHarbor((s) => s.milestones);
|
|
3413
3442
|
const { today } = useDeps();
|
|
3414
|
-
const
|
|
3443
|
+
const [showDone, setShowDone] = useState(false);
|
|
3444
|
+
const split = useMemo(() => splitDone(programs), [programs]);
|
|
3445
|
+
const listed = useMemo(() => showDone ? [...split.open, ...split.done] : split.open, [split, showDone]);
|
|
3446
|
+
const rows = useMemo(() => listed.map((program) => {
|
|
3415
3447
|
const portfolio = portfolios.find((p) => p.id === program.portfolioId);
|
|
3416
3448
|
const late = milestones.filter((m) => m.programId === program.id && !m.done && m.dueDate < today()).length;
|
|
3417
3449
|
return {
|
|
@@ -3427,16 +3459,17 @@ function ProgramsView({ active, width, height, go }) {
|
|
|
3427
3459
|
].filter(Boolean).join(" · ")
|
|
3428
3460
|
};
|
|
3429
3461
|
}), [
|
|
3430
|
-
|
|
3462
|
+
listed,
|
|
3431
3463
|
portfolios,
|
|
3432
3464
|
milestones,
|
|
3433
3465
|
today
|
|
3434
3466
|
]);
|
|
3467
|
+
const done = split.done.length;
|
|
3435
3468
|
return /* @__PURE__ */ jsx(RowList, {
|
|
3436
3469
|
title: "Programs",
|
|
3437
|
-
right: `${count(
|
|
3470
|
+
right: `${count(listed.length, "program")}${done ? ` · ${done} done ${showDone ? "shown" : "hidden"} (D)` : ""} · n new · e edit`,
|
|
3438
3471
|
rows,
|
|
3439
|
-
empty: "No programs yet. n adds one.",
|
|
3472
|
+
empty: done ? "Every program is done. D shows them." : "No programs yet. n adds one.",
|
|
3440
3473
|
active,
|
|
3441
3474
|
width,
|
|
3442
3475
|
height,
|
|
@@ -3446,7 +3479,8 @@ function ProgramsView({ active, width, height, go }) {
|
|
|
3446
3479
|
const portfolioId = r ? programs.find((p) => p.id === r.id)?.portfolioId : void 0;
|
|
3447
3480
|
go.create("program", portfolioId ? { portfolioId } : {});
|
|
3448
3481
|
},
|
|
3449
|
-
e: (r) => r && go.openPanel("program", r.id)
|
|
3482
|
+
e: (r) => r && go.openPanel("program", r.id),
|
|
3483
|
+
D: () => done > 0 && setShowDone((v) => !v)
|
|
3450
3484
|
}
|
|
3451
3485
|
});
|
|
3452
3486
|
}
|
|
@@ -4689,19 +4723,6 @@ function PersonPanel({ id, width, height, go, onClose }) {
|
|
|
4689
4723
|
});
|
|
4690
4724
|
}
|
|
4691
4725
|
|
|
4692
|
-
//#endregion
|
|
4693
|
-
//#region src/lib/selectors.ts
|
|
4694
|
-
/** Projects (or programs) still running, and the ones set to Done, which lists tuck away until asked for. */
|
|
4695
|
-
function splitDone(items) {
|
|
4696
|
-
const open = [];
|
|
4697
|
-
const done = [];
|
|
4698
|
-
for (const item of items) (item.health === "done" ? done : open).push(item);
|
|
4699
|
-
return {
|
|
4700
|
-
open,
|
|
4701
|
-
done
|
|
4702
|
-
};
|
|
4703
|
-
}
|
|
4704
|
-
|
|
4705
4726
|
//#endregion
|
|
4706
4727
|
//#region src/tui/views/program.tsx
|
|
4707
4728
|
/**
|
package/lib/cli.mjs
CHANGED
|
@@ -40,8 +40,9 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
40
40
|
harbor theme [auto|light|dark] the app's colours
|
|
41
41
|
harbor --version which version this is
|
|
42
42
|
|
|
43
|
-
harbor programs
|
|
44
|
-
harbor projects [words] [--program P]
|
|
43
|
+
harbor programs [--done] programs and their projects
|
|
44
|
+
harbor projects [words] [--program P] [--done]
|
|
45
|
+
projects with health and open work
|
|
45
46
|
harbor mine what's on you, late first
|
|
46
47
|
harbor tasks [words] [--project P] [--assignee me|none|NAME]
|
|
47
48
|
[--late] [--status S,S]
|
|
@@ -560,12 +561,12 @@ export async function run(argv, io = {}) {
|
|
|
560
561
|
return 0;
|
|
561
562
|
}
|
|
562
563
|
case "programs": {
|
|
563
|
-
const { positionals, harbor } = parse(spec());
|
|
564
|
+
const { positionals, values, harbor } = parse(spec(["done"]));
|
|
564
565
|
none(positionals);
|
|
565
|
-
return call("harbor_overview", { harbor }, programTree);
|
|
566
|
+
return call("harbor_overview", { harbor }, (r) => programTree(r, { done: values.done }));
|
|
566
567
|
}
|
|
567
568
|
case "projects": {
|
|
568
|
-
const { positionals, values, harbor } = parse(spec([], ["program"]));
|
|
569
|
+
const { positionals, values, harbor } = parse(spec(["done"], ["program"]));
|
|
569
570
|
return call(
|
|
570
571
|
"list_projects",
|
|
571
572
|
{
|
|
@@ -574,7 +575,7 @@ export async function run(argv, io = {}) {
|
|
|
574
575
|
program: values.program,
|
|
575
576
|
limit: 100,
|
|
576
577
|
},
|
|
577
|
-
projectList,
|
|
578
|
+
(r) => projectList(r, { done: values.done }),
|
|
578
579
|
);
|
|
579
580
|
}
|
|
580
581
|
case "program": {
|
package/lib/format.mjs
CHANGED
|
@@ -72,20 +72,33 @@ export function taskList(r, opts) {
|
|
|
72
72
|
const HEALTH = { "on-track": "on track", "at-risk": "at risk", "off-track": "off track" };
|
|
73
73
|
const health = (h) => HEALTH[h] ?? h ?? null;
|
|
74
74
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
75
|
+
/** "2 done not shown (--done shows them).", or nothing. */
|
|
76
|
+
const hiddenNote = (n, what) =>
|
|
77
|
+
n ? `\n${n} done ${what}${n === 1 ? "" : "s"} not shown (--done shows them).` : "";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Each portfolio's programs and their projects, indented, as harbor_overview
|
|
81
|
+
* gives them. Programs and projects set to Done stay out unless `done`.
|
|
82
|
+
*/
|
|
83
|
+
export function programTree(r, { done = false } = {}) {
|
|
77
84
|
const portfolios = r.portfolios ?? [];
|
|
78
85
|
if (!portfolios.some((pf) => pf.programs.length)) {
|
|
79
86
|
return 'No programs yet. Open one with harbor program "NAME".';
|
|
80
87
|
}
|
|
88
|
+
const shown = (x) => done || x.health !== "done";
|
|
89
|
+
let hidden = 0;
|
|
81
90
|
const out = [];
|
|
82
91
|
for (const pf of portfolios) {
|
|
83
|
-
|
|
92
|
+
const programs = pf.programs.filter(shown);
|
|
93
|
+
hidden += pf.programs.length - programs.length;
|
|
94
|
+
if (portfolios.length > 1 && programs.length) out.push(pf.name);
|
|
84
95
|
const pad = portfolios.length > 1 ? " " : "";
|
|
85
|
-
for (const pg of
|
|
96
|
+
for (const pg of programs) {
|
|
86
97
|
const bits = [health(pg.health), pg.ends ? `ends ${pg.ends}` : null].filter(Boolean);
|
|
87
98
|
out.push(`${pad}${pg.name} (${bits.join(" · ")})`);
|
|
88
|
-
|
|
99
|
+
const projects = pg.projects.filter(shown);
|
|
100
|
+
hidden += pg.projects.length - projects.length;
|
|
101
|
+
for (const pj of projects) {
|
|
89
102
|
const work = [
|
|
90
103
|
health(pj.health),
|
|
91
104
|
`${pj.openTasks} open`,
|
|
@@ -97,15 +110,20 @@ export function programTree(r) {
|
|
|
97
110
|
if (!pg.projects.length) out.push(`${pad} no projects yet`);
|
|
98
111
|
}
|
|
99
112
|
}
|
|
100
|
-
|
|
113
|
+
if (!out.length) return `Every program is done.${hiddenNote(hidden, "item").replace("\n", " ")}`;
|
|
114
|
+
return out.join("\n") + hiddenNote(hidden, "item");
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
/** Projects one per line, name first so it can be passed to --project. */
|
|
104
|
-
export function projectList(r) {
|
|
117
|
+
/** Projects one per line, name first so it can be passed to --project. Done ones only with `done`. */
|
|
118
|
+
export function projectList(r, { done = false } = {}) {
|
|
105
119
|
if (!r.projects?.length) return "No projects match.";
|
|
120
|
+
const projects = done ? r.projects : r.projects.filter((p) => p.health !== "done");
|
|
121
|
+
const hidden = r.projects.length - projects.length;
|
|
122
|
+
if (!projects.length)
|
|
123
|
+
return `Every project that matches is done.${hiddenNote(hidden, "project").replace("\n", " ")}`;
|
|
106
124
|
const more = r.total > r.projects.length ? `\n…and ${r.total - r.projects.length} more.` : "";
|
|
107
125
|
return (
|
|
108
|
-
|
|
126
|
+
projects
|
|
109
127
|
.map((p) => {
|
|
110
128
|
const late = p.tasks.late ? `, ${p.tasks.late} late` : "";
|
|
111
129
|
const bits = [
|
|
@@ -117,7 +135,9 @@ export function projectList(r) {
|
|
|
117
135
|
].filter(Boolean);
|
|
118
136
|
return `${p.name} (${bits.join(" · ")})`;
|
|
119
137
|
})
|
|
120
|
-
.join("\n") +
|
|
138
|
+
.join("\n") +
|
|
139
|
+
hiddenNote(hidden, "project") +
|
|
140
|
+
more
|
|
121
141
|
);
|
|
122
142
|
}
|
|
123
143
|
|