@harborppm/cli 0.9.1 → 0.9.3

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.
Files changed (2) hide show
  1. package/dist/app.mjs +62 -14
  2. package/package.json +1 -1
package/dist/app.mjs CHANGED
@@ -3068,11 +3068,12 @@ function inboxOf(data, personId, now = clockNow()) {
3068
3068
  return who ? firstName(who.name) : "Someone";
3069
3069
  }))];
3070
3070
  const blocked = waiting.length === 1 ? `“${waiting[0].title}”` : `${waiting.length} tasks`;
3071
+ const upstream = openBlockerNames(blocker, data.tasks);
3071
3072
  pings.push({
3072
3073
  id: waiting.length === 1 ? `wait:${waiting[0].id}` : `wait:${blocker.id}:${shortHash(waiting.map((t) => t.id).sort().join(","))}`,
3073
3074
  kind: "waiting",
3074
3075
  title: `${listNames(names)} ${names.length === 1 ? "is" : "are"} waiting on you`,
3075
- detail: `${blocked} ${waiting.length === 1 ? "is" : "are"} blocked by “${blocker.title}”${project ? ` · ${project.name}` : ""}`,
3076
+ detail: `${blocked} ${waiting.length === 1 ? "is" : "are"} blocked by “${blocker.title}”${project ? ` · ${project.name}` : ""}${upstream ? `. Yours waits on ${upstream} first.` : ""}`,
3076
3077
  at: waiting.reduce((latest, t) => t.createdAt > latest ? t.createdAt : latest, ""),
3077
3078
  taskId: blocker.id,
3078
3079
  personId: waiting[0].assigneeId,
@@ -3887,6 +3888,7 @@ const HELP = [
3887
3888
  ["u / r", "undo your last change / retry saving"],
3888
3889
  ["v", "on a project: board, list, budget"],
3889
3890
  ["m / R", "new milestone on a program / log a risk"],
3891
+ ["D", "show or hide Done projects and programs"],
3890
3892
  ["c / a", "on a task: comment / add a checklist step"],
3891
3893
  ["q", "quit (saves first)"]
3892
3894
  ];
@@ -4687,11 +4689,25 @@ function PersonPanel({ id, width, height, go, onClose }) {
4687
4689
  });
4688
4690
  }
4689
4691
 
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
+
4690
4705
  //#endregion
4691
4706
  //#region src/tui/views/program.tsx
4692
4707
  /**
4693
4708
  * One program: its health (and why, when set by hand), next move, its
4694
- * projects and its milestones. ←→ between the two lists.
4709
+ * projects and its milestones. ←→ between the two lists. Projects set to
4710
+ * Done stay out of the list until D shows them.
4695
4711
  */
4696
4712
  function ProgramView({ programId, active, width, height, go, onBack }) {
4697
4713
  const { store, today } = useDeps();
@@ -4701,7 +4717,9 @@ function ProgramView({ programId, active, width, height, go, onBack }) {
4701
4717
  const milestones = useHarbor((s) => s.milestones);
4702
4718
  const people = useHarbor((s) => s.people);
4703
4719
  const portfolio = useHarbor((s) => s.portfolios.find((p) => p.id === program?.portfolioId));
4704
- const mine = useMemo(() => projects.filter((p) => p.programId === programId), [projects, programId]);
4720
+ const [showDone, setShowDone] = useState(false);
4721
+ const split = useMemo(() => splitDone(projects.filter((p) => p.programId === programId)), [projects, programId]);
4722
+ const mine = showDone ? [...split.open, ...split.done] : split.open;
4705
4723
  const marks = useMemo(() => milestonesOf({ milestones }, programId, today()), [
4706
4724
  milestones,
4707
4725
  programId,
@@ -4720,6 +4738,10 @@ function ProgramView({ programId, active, width, height, go, onBack }) {
4720
4738
  if (input === "n") return go.create("project", { programId });
4721
4739
  if (input === "m") return go.create("milestone", { programId });
4722
4740
  if (input === "R") return go.create("raid", {});
4741
+ if (input === "D" && split.done.length > 0) {
4742
+ setSide("projects");
4743
+ return setShowDone((v) => !v);
4744
+ }
4723
4745
  if (side === "projects") {
4724
4746
  if (key.upArrow || input === "k") setPAt((i) => step(i, -1, mine.length));
4725
4747
  else if (key.downArrow || input === "j") setPAt((i) => step(i, 1, mine.length));
@@ -4794,12 +4816,12 @@ function ProgramView({ programId, active, width, height, go, onBack }) {
4794
4816
  marginRight: 1,
4795
4817
  children: [
4796
4818
  /* @__PURE__ */ jsx(Heading, {
4797
- right: `${mine.length}`,
4819
+ right: split.done.length > 0 ? `${mine.length} · ${split.done.length} done ${showDone ? "shown" : "hidden"} (D)` : `${mine.length}`,
4798
4820
  children: side === "projects" ? "› Projects" : " Projects"
4799
4821
  }),
4800
4822
  mine.length === 0 ? /* @__PURE__ */ jsx(Text, {
4801
4823
  color: theme.muted,
4802
- children: "None yet. n adds one."
4824
+ children: split.done.length > 0 ? "Every project here is done. D shows them." : "None yet. n adds one."
4803
4825
  }) : null,
4804
4826
  windowOf(mine, pAt, rows).items.map((p) => {
4805
4827
  const on = active && side === "projects" && p.id === project?.id;
@@ -4873,18 +4895,21 @@ function ProgramView({ programId, active, width, height, go, onBack }) {
4873
4895
  ]
4874
4896
  });
4875
4897
  }
4876
- /** One portfolio: its health and its programs, each with theirs. */
4898
+ /** One portfolio: its health and its programs, each with theirs; Done ones wait for D. */
4877
4899
  function PortfolioView({ portfolioId, active, width, height, go, onBack }) {
4878
4900
  const portfolio = useHarbor((s) => s.portfolios.find((p) => p.id === portfolioId));
4879
4901
  const programs = useHarbor((s) => s.programs);
4880
4902
  const projects = useHarbor((s) => s.projects);
4881
- const list = useMemo(() => programs.filter((p) => p.portfolioId === portfolioId), [programs, portfolioId]);
4903
+ const [showDone, setShowDone] = useState(false);
4904
+ const split = useMemo(() => splitDone(programs.filter((p) => p.portfolioId === portfolioId)), [programs, portfolioId]);
4905
+ const list = showDone ? [...split.open, ...split.done] : split.open;
4882
4906
  const [at, setAt] = useState(0);
4883
4907
  const program = list[Math.min(at, list.length - 1)];
4884
4908
  useInput((input, key) => {
4885
4909
  if (key.escape || key.backspace) return onBack();
4886
4910
  if (input === "e") return go.openPanel("portfolio", portfolioId);
4887
4911
  if (input === "n") return go.create("program", { portfolioId });
4912
+ if (input === "D" && split.done.length > 0) return setShowDone((v) => !v);
4888
4913
  if (key.upArrow || input === "k") setAt((i) => step(i, -1, list.length));
4889
4914
  else if (key.downArrow || input === "j") setAt((i) => step(i, 1, list.length));
4890
4915
  else if (key.return && program) go.openProgram(program.id);
@@ -4928,12 +4953,12 @@ function PortfolioView({ portfolioId, active, width, height, go, onBack }) {
4928
4953
  }),
4929
4954
  /* @__PURE__ */ jsx(Text, { children: " " }),
4930
4955
  /* @__PURE__ */ jsx(Heading, {
4931
- right: `${list.length} program${list.length === 1 ? "" : "s"}`,
4956
+ right: `${list.length} program${list.length === 1 ? "" : "s"}${split.done.length > 0 ? ` · ${split.done.length} done ${showDone ? "shown" : "hidden"} (D)` : ""}`,
4932
4957
  children: "Programs"
4933
4958
  }),
4934
4959
  list.length === 0 ? /* @__PURE__ */ jsx(Text, {
4935
4960
  color: theme.muted,
4936
- children: "None yet. n adds one."
4961
+ children: split.done.length > 0 ? "Every program here is done. D shows them." : "None yet. n adds one."
4937
4962
  }) : null,
4938
4963
  windowOf(list, at, height - 5).items.map((p) => {
4939
4964
  const on = active && p.id === program?.id;
@@ -5490,7 +5515,7 @@ function TaskPanel({ taskId, width, height, go, onClose }) {
5490
5515
  const projects = useHarbor((s) => s.projects);
5491
5516
  const people = useHarbor((s) => s.people);
5492
5517
  const comments = useHarbor((s) => s.comments);
5493
- const viewOnly = useHarbor((s) => s.myRole === "viewer");
5518
+ const viewOnly = useHarbor((s) => s.myRole === "viewer" || s.keepPaused);
5494
5519
  const task = tasks.find((t) => t.id === taskId);
5495
5520
  const [at, setAt] = useState(0);
5496
5521
  const [editing, setEditing] = useState(null);
@@ -5932,6 +5957,7 @@ function App({ ui }) {
5932
5957
  const harborName = useHarbor((s) => s.harborName);
5933
5958
  const workspaceId = useHarbor((s) => s.workspaceId);
5934
5959
  const viewOnly = useHarbor((s) => s.myRole === "viewer");
5960
+ const paused = useHarbor((s) => s.keepPaused);
5935
5961
  const blockPending = useHarbor((s) => s.blockPending);
5936
5962
  useHarbor((s) => s.readPingIds?.length ?? 0);
5937
5963
  const save = useStore(ui, (s) => s.save);
@@ -6347,7 +6373,10 @@ function App({ ui }) {
6347
6373
  bold: true,
6348
6374
  children: harborName ?? "…"
6349
6375
  }),
6350
- viewOnly ? /* @__PURE__ */ jsx(Text, {
6376
+ paused ? /* @__PURE__ */ jsx(Text, {
6377
+ color: theme.warn,
6378
+ children: " · trial ended, view only"
6379
+ }) : viewOnly ? /* @__PURE__ */ jsx(Text, {
6351
6380
  color: theme.warn,
6352
6381
  children: " · view only"
6353
6382
  }) : null
@@ -8404,6 +8433,9 @@ function peopleSpendSlice({ set, get, write }) {
8404
8433
  * as in a browser tab. No persistence: the server holds the harbor, and
8405
8434
  * sync.ts keeps this copy in step with it.
8406
8435
  */
8436
+ /** What the store tells the screen: a refused edit, an undo that kept something. */
8437
+ /** Why nothing changed in a harbor whose free trial has ended (keep.ts). */
8438
+ const PAUSED_NOTICE = "This harbor's free trial has ended, so that wasn't changed. Its owner can subscribe at harborppm.com/keep.";
8407
8439
  /** An empty harbor: what the store holds before one loads. */
8408
8440
  function blank() {
8409
8441
  return pickData({ currentUserId: "" });
@@ -8413,13 +8445,16 @@ function createAppStore(onNotice = () => {}) {
8413
8445
  const store = createStore()((set, get) => {
8414
8446
  const guardedSet = (partial, replace) => {
8415
8447
  const s = get();
8416
- if (s.myRole !== "viewer") {
8448
+ if (!readOnlyHarbor(s)) {
8417
8449
  set(partial, replace);
8418
8450
  return;
8419
8451
  }
8420
8452
  const next = typeof partial === "function" ? partial(s) : partial;
8421
8453
  const { allowed, blocked } = viewerUpdate(s, next);
8422
- if (blocked) onNotice({ kind: "view-only" });
8454
+ if (blocked) onNotice({
8455
+ kind: "view-only",
8456
+ paused: s.keepPaused
8457
+ });
8423
8458
  set(allowed);
8424
8459
  };
8425
8460
  const write = (partial) => {
@@ -8453,6 +8488,8 @@ function createAppStore(onNotice = () => {}) {
8453
8488
  kept: false,
8454
8489
  keepInterval: null,
8455
8490
  stripeReady: false,
8491
+ keepPaused: false,
8492
+ trialEndsAt: null,
8456
8493
  wipPending: null,
8457
8494
  blockPending: null,
8458
8495
  ...sessionSlice(api),
@@ -9025,6 +9062,16 @@ function createSync(opts) {
9025
9062
  if (told) notice(told.message);
9026
9063
  if (result.conflictIds.length > 0) notice("Someone else saved changes here; pulled in the latest.");
9027
9064
  }
9065
+ } else if ("paused" in result && result.paused) {
9066
+ const now = store.getState();
9067
+ now.setKeep({
9068
+ kept: false,
9069
+ interval: now.keepInterval,
9070
+ stripe: now.stripeReady,
9071
+ trialEndsAt: now.trialEndsAt,
9072
+ paused: true
9073
+ });
9074
+ notice(PAUSED_NOTICE);
9028
9075
  } else if ("readOnly" in result && result.readOnly) {
9029
9076
  store.getState().setMyRole("viewer");
9030
9077
  notice("You can only view this harbor now, so that change wasn't saved.");
@@ -9112,6 +9159,7 @@ function createSync(opts) {
9112
9159
  s.setWorkspace(loaded.id, true);
9113
9160
  s.setWorkspaceVersion(loaded.version);
9114
9161
  s.setMyRole(normalizeRole(loaded.role));
9162
+ if (loaded.keep) s.setKeep(loaded.keep);
9115
9163
  s.setHarborName(loaded.name);
9116
9164
  s.setPhase("ready");
9117
9165
  });
@@ -9296,7 +9344,7 @@ async function startApp(deps, options = {}) {
9296
9344
  const account = plainDeep(deps.account);
9297
9345
  const harbors = plainDeep(deps.harbors);
9298
9346
  const ui = createUiStore();
9299
- const store = createAppStore((n) => ui.setState({ notice: n.kind === "view-only" ? "You can only view this harbor, so that wasn't changed." : n.kept > 0 ? `Undid ${n.label}. ${n.kept} field${n.kept === 1 ? "" : "s"} someone else changed since stayed theirs.` : `Undid ${n.label}.` }));
9347
+ const store = createAppStore((n) => ui.setState({ notice: n.kind === "view-only" ? n.paused ? PAUSED_NOTICE : "You can only view this harbor, so that wasn't changed." : n.kept > 0 ? `Undid ${n.label}. ${n.kept} field${n.kept === 1 ? "" : "s"} someone else changed since stayed theirs.` : `Undid ${n.label}.` }));
9300
9348
  const sync = createSync({
9301
9349
  store,
9302
9350
  transport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Harbor from the terminal: a full-screen app with everything, plus commands for scripts and agents.",
5
5
  "keywords": [
6
6
  "harbor",