@harborppm/cli 0.87.0 → 0.89.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 CHANGED
@@ -4944,7 +4944,7 @@ function normalizeBoardConfig(raw) {
4944
4944
  const todo = columns.find((c) => c.status === "todo") ?? columns[0];
4945
4945
  todo.visible = true;
4946
4946
  }
4947
- const swimlane = raw.swimlane === "assignee" || raw.swimlane === "priority" ? raw.swimlane : "none";
4947
+ const swimlane = raw.swimlane === "assignee" || raw.swimlane === "priority" ? raw.swimlane : typeof raw.swimlane === "string" && /^field:[^\s]{1,100}$/.test(raw.swimlane) ? raw.swimlane : "none";
4948
4948
  const cardFields = {
4949
4949
  dueDate: raw.cardFields?.dueDate !== false,
4950
4950
  assignee: raw.cardFields?.assignee !== false,
@@ -5066,6 +5066,22 @@ function isVoyageProject(projectId) {
5066
5066
  function ownProjects(projects) {
5067
5067
  return projects.filter((p) => !isVoyageProject(p.id));
5068
5068
  }
5069
+ /**
5070
+ * The harbor as anyone outside it may see it: First voyage is practice, so
5071
+ * share links, their embeds and the Monday letter to stakeholders leave it
5072
+ * out, with its tasks, risks and spend, from what they show and count.
5073
+ */
5074
+ function withoutPractice(data) {
5075
+ if (!data.projects.some((p) => isVoyageProject(p.id))) return data;
5076
+ const out = {
5077
+ ...data,
5078
+ projects: ownProjects(data.projects),
5079
+ tasks: data.tasks.filter((t) => !isVoyageProject(t.projectId)),
5080
+ raidItems: data.raidItems.filter((r) => !r.projectId || !isVoyageProject(r.projectId))
5081
+ };
5082
+ if (data.spendEntries) out.spendEntries = data.spendEntries.filter((e) => !isVoyageProject(e.projectId));
5083
+ return out;
5084
+ }
5069
5085
  var VOYAGE_PROJECT_ID, STEPS, voyageTaskId, VOYAGE_NO_FORECAST, SEEDED;
5070
5086
  var init_first_voyage = __esmMin((() => {
5071
5087
  VOYAGE_PROJECT_ID = "voyage";
@@ -8205,6 +8221,7 @@ function list$2(names) {
8205
8221
  * document only; nothing here changes it.
8206
8222
  */
8207
8223
  function shippingForecast(data, opts) {
8224
+ data = withoutPractice(data);
8208
8225
  const { now } = opts;
8209
8226
  const today = todayISO(now);
8210
8227
  const programs = data.programs.filter((p) => !opts.portfolioId || p.portfolioId === opts.portfolioId);
@@ -8291,6 +8308,7 @@ var init_shipping_forecast = __esmMin((() => {
8291
8308
  init_date_text();
8292
8309
  init_format();
8293
8310
  init_indexes();
8311
+ init_first_voyage();
8294
8312
  FORECAST_AREAS = 10;
8295
8313
  WORDS = [
8296
8314
  "no",
@@ -10022,7 +10040,9 @@ const KIND_RANK = {
10022
10040
  sounding: 3,
10023
10041
  assigned: 4,
10024
10042
  idle: 5,
10025
- mention: 6
10043
+ mention: 6,
10044
+ reply: 7,
10045
+ finished: 8
10026
10046
  };
10027
10047
  const PING_KIND_LABEL = {
10028
10048
  harbor: "Your harbor",
@@ -10032,7 +10052,9 @@ const PING_KIND_LABEL = {
10032
10052
  sounding: "Soundings",
10033
10053
  assigned: "Assigned to you",
10034
10054
  idle: "Quiet blockers",
10035
- mention: "Mentions"
10055
+ mention: "Mentions",
10056
+ reply: "Replies",
10057
+ finished: "Finished"
10036
10058
  };
10037
10059
  function firstName$3(name) {
10038
10060
  return name.split(/\s+/)[0] ?? name;
@@ -10240,6 +10262,63 @@ function inboxOf(data, personId, now = clockNow()) {
10240
10262
  href: `/projects/${task.projectId}`
10241
10263
  });
10242
10264
  }
10265
+ const recentByTask = /* @__PURE__ */ new Map();
10266
+ for (const c of data.comments) {
10267
+ const list = recentByTask.get(c.taskId);
10268
+ if (list) list.push(c);
10269
+ else recentByTask.set(c.taskId, [c]);
10270
+ }
10271
+ for (const [taskId, thread] of recentByTask) {
10272
+ const task = byId(data.tasks).get(taskId);
10273
+ if (!task) continue;
10274
+ const ordered = [...thread].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
10275
+ let joined = task.assignedBy === personId && task.assigneeId !== personId;
10276
+ const replies = [];
10277
+ for (const c of ordered) {
10278
+ if (c.authorId === personId) {
10279
+ joined = true;
10280
+ replies.length = 0;
10281
+ continue;
10282
+ }
10283
+ if (!joined || c.createdAt < since) continue;
10284
+ if ((c.mentionedIds ?? []).includes(personId)) continue;
10285
+ replies.push(c);
10286
+ }
10287
+ const latest = replies.at(-1);
10288
+ if (!latest) continue;
10289
+ const authors = [...new Set(replies.map((c) => {
10290
+ const who = data.people.find((p) => p.id === c.authorId);
10291
+ return who ? firstName$3(who.name) : "Someone";
10292
+ }))];
10293
+ const project = projectOf(data, task);
10294
+ const snippet = excerpt(latest.body);
10295
+ pings.push({
10296
+ id: `reply:${latest.id}`,
10297
+ kind: "reply",
10298
+ title: `${listNames(authors)} replied`,
10299
+ detail: snippet ? `on “${task.title}”${project ? ` · ${project.name}` : ""} — ${snippet}` : `on “${task.title}”${project ? ` · ${project.name}` : ""}`,
10300
+ at: latest.createdAt,
10301
+ taskId: task.id,
10302
+ personId: latest.authorId,
10303
+ href: `/projects/${task.projectId}`
10304
+ });
10305
+ }
10306
+ for (const task of data.tasks) {
10307
+ if (task.assignedBy !== personId || !task.assigneeId || task.assigneeId === personId) continue;
10308
+ if (task.status !== "done" || !task.completedAt || task.completedAt < since) continue;
10309
+ const who = data.people.find((p) => p.id === task.assigneeId);
10310
+ const project = projectOf(data, task);
10311
+ pings.push({
10312
+ id: `finished:${task.id}:${task.completedAt}`,
10313
+ kind: "finished",
10314
+ title: `${who ? firstName$3(who.name) : "Someone"} finished “${task.title}”`,
10315
+ detail: project ? `A task you gave them · ${project.name}` : "A task you gave them",
10316
+ at: task.completedAt,
10317
+ taskId: task.id,
10318
+ personId: task.assigneeId,
10319
+ href: `/projects/${task.projectId}`
10320
+ });
10321
+ }
10243
10322
  return pings.sort((a, b) => {
10244
10323
  const rank = KIND_RANK[a.kind] - KIND_RANK[b.kind];
10245
10324
  if (rank !== 0) return rank;
@@ -29932,6 +30011,15 @@ function defaultAction(type) {
29932
30011
  };
29933
30012
  }
29934
30013
 
30014
+ // src/lib/board-lanes.ts
30015
+ /** Fields a board can be split into lanes by. */
30016
+ function isLaneField(def) {
30017
+ return def.type === "select" || def.type === "multi" || def.type === "person" || def.type === "checkbox" || def.type === "date";
30018
+ }
30019
+ function fieldLane(fieldId) {
30020
+ return `field:${fieldId}`;
30021
+ }
30022
+
29935
30023
  // src/tui/views/setup.tsx
29936
30024
  /**
29937
30025
  * A project's board setup, as the website's board settings have it: where it
@@ -29998,6 +30086,10 @@ function BoardPanel({ id, width, height, go, active, onClose }) {
29998
30086
  const people = useHarbor((s) => s.people);
29999
30087
  if (!project) return null;
30000
30088
  const config = boardConfigOf({ boardConfigs }, id);
30089
+ const lanes = [...LANES, ...(project.customFields ?? []).filter(isLaneField).map((f) => ({
30090
+ value: fieldLane(f.id),
30091
+ label: `By ${f.name}`
30092
+ }))];
30001
30093
  const set = (patch) => store.getState().setBoardConfig(id, {
30002
30094
  ...config,
30003
30095
  preset: "custom",
@@ -30080,15 +30172,15 @@ function BoardPanel({ id, width, height, go, active, onClose }) {
30080
30172
  ...config.columns.map(columnField),
30081
30173
  {
30082
30174
  label: "Lanes",
30083
- value: LANES.find((l) => l.value === config.swimlane)?.label ?? "None",
30175
+ value: lanes.find((l) => l.value === config.swimlane)?.label ?? "None",
30084
30176
  edit: {
30085
30177
  kind: "pick",
30086
30178
  title: "Lanes across the website's board",
30087
- choices: LANES.map((l) => ({
30179
+ choices: lanes.map((l) => ({
30088
30180
  label: l.label,
30089
30181
  value: l.value
30090
30182
  })),
30091
- initial: LANES.findIndex((l) => l.value === config.swimlane),
30183
+ initial: Math.max(0, lanes.findIndex((l) => l.value === config.swimlane)),
30092
30184
  onPick: (v) => set({ swimlane: v })
30093
30185
  }
30094
30186
  },
package/lib/format.mjs CHANGED
@@ -769,6 +769,12 @@ export function importText(r) {
769
769
  if (r.alreadyThere) lines.push(`${n(r.alreadyThere, "task")} already there, left alone.`);
770
770
  if (r.leftOut) lines.push(`${n(r.leftOut, "row")} left out (canceled, won't do or duplicate).`);
771
771
  if (r.waitsOnLinks) lines.push(`${n(r.waitsOnLinks, "link")} between tasks became "waits on".`);
772
+ if (r.subtasks)
773
+ lines.push(
774
+ r.subtasks === 1
775
+ ? "1 row came in as a subtask of its task."
776
+ : `${r.subtasks} rows came in as subtasks of their tasks.`,
777
+ );
772
778
  for (const p of r.problems ?? [])
773
779
  lines.push(`${p.card ? `Card “${p.card}”` : `Row ${p.row}`}: ${p.message}`);
774
780
  return lines.join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.87.0",
3
+ "version": "0.89.0",
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",