@harborppm/cli 0.91.0 → 0.92.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 +583 -149
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -9604,12 +9604,14 @@ const NOUN$1 = {
|
|
|
9604
9604
|
milestone: "milestone",
|
|
9605
9605
|
risk: "risk",
|
|
9606
9606
|
issue: "issue",
|
|
9607
|
-
decision: "decision"
|
|
9607
|
+
decision: "decision",
|
|
9608
|
+
hold: ""
|
|
9608
9609
|
};
|
|
9609
9610
|
const CREATED = {
|
|
9610
9611
|
risk: "raised risk",
|
|
9611
9612
|
issue: "logged issue",
|
|
9612
|
-
decision: "recorded decision"
|
|
9613
|
+
decision: "recorded decision",
|
|
9614
|
+
hold: "named what's holding it back:"
|
|
9613
9615
|
};
|
|
9614
9616
|
const whole = new Intl.NumberFormat("en-US", {
|
|
9615
9617
|
style: "currency",
|
|
@@ -9667,23 +9669,31 @@ function describeEvent(e, names) {
|
|
|
9667
9669
|
detail: null
|
|
9668
9670
|
};
|
|
9669
9671
|
if (e.kind === "deleted") return {
|
|
9670
|
-
action: `deleted ${noun}
|
|
9672
|
+
action: `deleted ${noun}`.trim(),
|
|
9671
9673
|
detail: null
|
|
9672
9674
|
};
|
|
9673
9675
|
const { from, to } = e;
|
|
9674
9676
|
switch (e.field) {
|
|
9675
9677
|
case "name":
|
|
9676
9678
|
case "title": return {
|
|
9677
|
-
action: `renamed ${noun}
|
|
9679
|
+
action: `renamed ${noun}`.trim(),
|
|
9678
9680
|
detail: from ? `from “${from}”` : null
|
|
9679
9681
|
};
|
|
9680
|
-
case "status":
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9682
|
+
case "status":
|
|
9683
|
+
if (e.entity === "hold") return to === "resolved" ? {
|
|
9684
|
+
action: "cleared",
|
|
9685
|
+
detail: "from what's holding it back"
|
|
9686
|
+
} : {
|
|
9687
|
+
action: "brought back",
|
|
9688
|
+
detail: "as what's holding it back"
|
|
9689
|
+
};
|
|
9690
|
+
return e.entity === "task" ? {
|
|
9691
|
+
action: "moved",
|
|
9692
|
+
detail: `from ${statusLabel(e.entity, from)} to ${statusLabel(e.entity, to)}`
|
|
9693
|
+
} : {
|
|
9694
|
+
action: "set",
|
|
9695
|
+
detail: `to ${statusLabel(e.entity, to)}`
|
|
9696
|
+
};
|
|
9687
9697
|
case "assigneeId": return to ? {
|
|
9688
9698
|
action: "assigned",
|
|
9689
9699
|
detail: `to ${nameOf$3(names, to)}`
|
|
@@ -11070,6 +11080,46 @@ function blockerOptions(items, id) {
|
|
|
11070
11080
|
return items.filter((n) => n.id !== id && !loops.has(n.id));
|
|
11071
11081
|
}
|
|
11072
11082
|
|
|
11083
|
+
// src/lib/constraint-history.ts
|
|
11084
|
+
const RETIRED = "retired the constraint on ";
|
|
11085
|
+
function clearedConstraints(data, programId) {
|
|
11086
|
+
const rows = (data.constraints ?? []).filter((c) => c.programId === programId && c.status === "resolved");
|
|
11087
|
+
const unstamped = rows.filter((c) => !c.resolvedAt);
|
|
11088
|
+
let guess;
|
|
11089
|
+
if (unstamped.length === 1) guess = (data.activity ?? []).find((a) => a.text.startsWith(RETIRED) && a.href === `/programs/${programId}`);
|
|
11090
|
+
const out = rows.map((c, i) => {
|
|
11091
|
+
if (c.resolvedAt) return {
|
|
11092
|
+
constraint: c,
|
|
11093
|
+
at: c.resolvedAt,
|
|
11094
|
+
byId: c.resolvedById ?? null,
|
|
11095
|
+
order: i
|
|
11096
|
+
};
|
|
11097
|
+
if (guess) return {
|
|
11098
|
+
constraint: c,
|
|
11099
|
+
at: guess.at,
|
|
11100
|
+
byId: guess.personId || null,
|
|
11101
|
+
order: i
|
|
11102
|
+
};
|
|
11103
|
+
return {
|
|
11104
|
+
constraint: c,
|
|
11105
|
+
at: null,
|
|
11106
|
+
byId: null,
|
|
11107
|
+
order: i
|
|
11108
|
+
};
|
|
11109
|
+
});
|
|
11110
|
+
out.sort((a, b) => {
|
|
11111
|
+
if (a.at && b.at) return a.at < b.at ? 1 : a.at > b.at ? -1 : b.order - a.order;
|
|
11112
|
+
if (a.at) return -1;
|
|
11113
|
+
if (b.at) return 1;
|
|
11114
|
+
return b.order - a.order;
|
|
11115
|
+
});
|
|
11116
|
+
return out.map(({ constraint, at, byId }) => ({
|
|
11117
|
+
constraint,
|
|
11118
|
+
at,
|
|
11119
|
+
byId
|
|
11120
|
+
}));
|
|
11121
|
+
}
|
|
11122
|
+
|
|
11073
11123
|
// src/lib/status-report.ts
|
|
11074
11124
|
init_derived_health();
|
|
11075
11125
|
init_flow();
|
|
@@ -11426,7 +11476,19 @@ function ProgramPanel({ id, width, height, go, active, onClose }) {
|
|
|
11426
11476
|
const programs = useHarbor((s) => s.programs);
|
|
11427
11477
|
const projects = useHarbor((s) => s.projects);
|
|
11428
11478
|
const constraint = useHarbor((s) => s.constraints.find((c) => c.programId === id && c.status === "active"));
|
|
11479
|
+
const constraints = useHarbor((s) => s.constraints);
|
|
11480
|
+
const activity = useHarbor((s) => s.activity);
|
|
11429
11481
|
if (!program) return null;
|
|
11482
|
+
const cleared = clearedConstraints({
|
|
11483
|
+
constraints,
|
|
11484
|
+
activity
|
|
11485
|
+
}, id);
|
|
11486
|
+
const clearedHint = (at, byId) => {
|
|
11487
|
+
const when = at ? parseIso(at) : null;
|
|
11488
|
+
const by = byId ? people.find((p) => p.id === byId)?.name : void 0;
|
|
11489
|
+
const ago = when && !Number.isNaN(when.getTime()) ? `cleared ${timeAgo(when)}` : "cleared, date not kept";
|
|
11490
|
+
return by ? `${ago} by ${by}` : ago;
|
|
11491
|
+
};
|
|
11430
11492
|
const s = store.getState();
|
|
11431
11493
|
const inProgram = projects.filter((p) => p.programId === id);
|
|
11432
11494
|
const baselined = inProgram.filter(hasBaseline);
|
|
@@ -11553,6 +11615,22 @@ function ProgramPanel({ id, width, height, go, active, onClose }) {
|
|
|
11553
11615
|
onPick: (v) => holding({ ownerId: v })
|
|
11554
11616
|
}
|
|
11555
11617
|
}] : [],
|
|
11618
|
+
...cleared.length ? [{
|
|
11619
|
+
label: "Cleared before",
|
|
11620
|
+
value: `${cleared[0].constraint.title} (${clearedHint(cleared[0].at, cleared[0].byId)})${cleared.length > 1 ? ` and ${cleared.length - 1} more` : ""}`,
|
|
11621
|
+
color: theme.muted,
|
|
11622
|
+
edit: constraint ? void 0 : {
|
|
11623
|
+
kind: "pick",
|
|
11624
|
+
title: "Bring one back as what's holding it back",
|
|
11625
|
+
choices: cleared.map((c) => ({
|
|
11626
|
+
label: c.constraint.title,
|
|
11627
|
+
value: c.constraint.id,
|
|
11628
|
+
hint: clearedHint(c.at, c.byId)
|
|
11629
|
+
})),
|
|
11630
|
+
initial: 0,
|
|
11631
|
+
onPick: (v) => store.getState().reopenConstraint(v)
|
|
11632
|
+
}
|
|
11633
|
+
}] : [],
|
|
11556
11634
|
waitsField("Waits on", program, programs, go, (patch) => store.getState().updateProgram(id, patch)),
|
|
11557
11635
|
{
|
|
11558
11636
|
label: "Baseline",
|
|
@@ -14076,8 +14154,88 @@ function keyResultText(kr, work) {
|
|
|
14076
14154
|
}
|
|
14077
14155
|
const fmt = (n) => Number.isInteger(n) ? n.toLocaleString("en-US") : String(Math.round(n * 100) / 100);
|
|
14078
14156
|
|
|
14157
|
+
// src/lib/custom-field-text.ts
|
|
14158
|
+
init_date_text();
|
|
14159
|
+
const FIELD_TYPE_LABEL = {
|
|
14160
|
+
text: "Text",
|
|
14161
|
+
number: "Number",
|
|
14162
|
+
select: "Dropdown",
|
|
14163
|
+
multi: "Tags",
|
|
14164
|
+
checkbox: "Checkbox",
|
|
14165
|
+
date: "Date",
|
|
14166
|
+
person: "Person",
|
|
14167
|
+
url: "Link"
|
|
14168
|
+
};
|
|
14169
|
+
const FIELD_TYPES = Object.keys(FIELD_TYPE_LABEL);
|
|
14170
|
+
/** A dropdown or tags field: one with a list of options. */
|
|
14171
|
+
function hasOptions(type) {
|
|
14172
|
+
return type === "select" || type === "multi";
|
|
14173
|
+
}
|
|
14174
|
+
/** A dropdown's options from "S, M, L". */
|
|
14175
|
+
function optionsOf(value) {
|
|
14176
|
+
return [...new Set(value.split(",").map((o) => o.trim()).filter(Boolean))];
|
|
14177
|
+
}
|
|
14178
|
+
/** The link a value names when it's an http(s) address, else null (never javascript: and the like). */
|
|
14179
|
+
function safeLink(value) {
|
|
14180
|
+
if (typeof value !== "string") return null;
|
|
14181
|
+
try {
|
|
14182
|
+
const u = new URL(value.trim());
|
|
14183
|
+
return u.protocol === "https:" || u.protocol === "http:" ? u.href : null;
|
|
14184
|
+
} catch {
|
|
14185
|
+
return null;
|
|
14186
|
+
}
|
|
14187
|
+
}
|
|
14188
|
+
/** A link from "example.com/x" or "https://…"; null when it isn't a web address. */
|
|
14189
|
+
function readLink(text) {
|
|
14190
|
+
const t = text.trim();
|
|
14191
|
+
if (!t || /\s/.test(t)) return null;
|
|
14192
|
+
const href = safeLink(/^[a-z][a-z0-9+.-]*:/i.test(t) ? t : `https://${t}`);
|
|
14193
|
+
return href && /\./.test(new URL(href).hostname) ? href : null;
|
|
14194
|
+
}
|
|
14195
|
+
/** "example.com/docs/plan" from a link: what a short chip says. */
|
|
14196
|
+
function linkLabel(href) {
|
|
14197
|
+
try {
|
|
14198
|
+
const u = new URL(href);
|
|
14199
|
+
const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
|
|
14200
|
+
const text = u.host.replace(/^www\./, "") + path;
|
|
14201
|
+
return text.length > 40 ? `${text.slice(0, 39)}…` : text;
|
|
14202
|
+
} catch {
|
|
14203
|
+
return href;
|
|
14204
|
+
}
|
|
14205
|
+
}
|
|
14206
|
+
/** A tags field's value as a list, whatever was stored. */
|
|
14207
|
+
function tagsOf(value) {
|
|
14208
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
14209
|
+
return typeof value === "string" && value ? [value] : [];
|
|
14210
|
+
}
|
|
14211
|
+
/** Whether a task carries a value for the field. */
|
|
14212
|
+
function hasFieldValue(def, value) {
|
|
14213
|
+
if (value == null || value === "" || value === false) return false;
|
|
14214
|
+
if (def.type === "multi") return tagsOf(value).length > 0;
|
|
14215
|
+
return true;
|
|
14216
|
+
}
|
|
14217
|
+
/** A task's value for a field as one line: "" when it has none. */
|
|
14218
|
+
function fieldValueText(def, value, nameOf) {
|
|
14219
|
+
if (!hasFieldValue(def, value)) return "";
|
|
14220
|
+
switch (def.type) {
|
|
14221
|
+
case "checkbox": return value ? "Yes" : "";
|
|
14222
|
+
case "multi": return tagsOf(value).join(", ");
|
|
14223
|
+
case "date": {
|
|
14224
|
+
const d = parseIso(String(value));
|
|
14225
|
+
return Number.isNaN(d.getTime()) ? String(value) : format(d, "MMM d, yyyy");
|
|
14226
|
+
}
|
|
14227
|
+
case "person": return nameOf?.(String(value)) ?? "Someone no longer here";
|
|
14228
|
+
case "url": {
|
|
14229
|
+
const href = safeLink(value);
|
|
14230
|
+
return href ? linkLabel(href) : "";
|
|
14231
|
+
}
|
|
14232
|
+
default: return Array.isArray(value) ? value.join(", ") : String(value);
|
|
14233
|
+
}
|
|
14234
|
+
}
|
|
14235
|
+
|
|
14079
14236
|
// src/lib/reports.ts
|
|
14080
14237
|
init_date_fns();
|
|
14238
|
+
init_blockers();
|
|
14081
14239
|
init_date_text();
|
|
14082
14240
|
init_indexes();
|
|
14083
14241
|
init_swatches();
|
|
@@ -14086,20 +14244,29 @@ const WEEKS_BACK = 12;
|
|
|
14086
14244
|
const WEEKS_AHEAD = 8;
|
|
14087
14245
|
/** Bars past this many are summed into "Everything else". */
|
|
14088
14246
|
const MAX_BARS = 12;
|
|
14089
|
-
const
|
|
14090
|
-
"
|
|
14091
|
-
"
|
|
14092
|
-
"
|
|
14093
|
-
"
|
|
14094
|
-
"
|
|
14095
|
-
"
|
|
14247
|
+
const MEASURES = [
|
|
14248
|
+
"open",
|
|
14249
|
+
"done",
|
|
14250
|
+
"all",
|
|
14251
|
+
"overdue",
|
|
14252
|
+
"blocked",
|
|
14253
|
+
"approval",
|
|
14254
|
+
"hours",
|
|
14255
|
+
"risks",
|
|
14256
|
+
"issues",
|
|
14257
|
+
"decisions"
|
|
14096
14258
|
];
|
|
14097
14259
|
const MEASURE_LABEL = {
|
|
14098
14260
|
open: "Open tasks",
|
|
14099
14261
|
done: "Finished tasks",
|
|
14100
14262
|
all: "All tasks",
|
|
14101
14263
|
overdue: "Overdue tasks",
|
|
14102
|
-
|
|
14264
|
+
blocked: "Blocked tasks",
|
|
14265
|
+
approval: "Tasks waiting for approval",
|
|
14266
|
+
hours: "Hours logged",
|
|
14267
|
+
risks: "Open risks",
|
|
14268
|
+
issues: "Open issues",
|
|
14269
|
+
decisions: "Decisions to make"
|
|
14103
14270
|
};
|
|
14104
14271
|
const GROUP_LABEL = {
|
|
14105
14272
|
status: "Status",
|
|
@@ -14107,16 +14274,72 @@ const GROUP_LABEL = {
|
|
|
14107
14274
|
priority: "Priority",
|
|
14108
14275
|
project: "Project",
|
|
14109
14276
|
program: "Program",
|
|
14110
|
-
week: "Week"
|
|
14277
|
+
week: "Week",
|
|
14278
|
+
severity: "Severity",
|
|
14279
|
+
field: "A project field"
|
|
14280
|
+
};
|
|
14281
|
+
/** Still open: raid.ts's `isActive`, kept here so a chart doesn't load the register's code. */
|
|
14282
|
+
const ACTIVE_RAID = /* @__PURE__ */ new Set([
|
|
14283
|
+
"open",
|
|
14284
|
+
"watching",
|
|
14285
|
+
"mitigated",
|
|
14286
|
+
"proposed"
|
|
14287
|
+
]);
|
|
14288
|
+
const RAID_KIND = {
|
|
14289
|
+
risks: "risk",
|
|
14290
|
+
issues: "issue",
|
|
14291
|
+
decisions: "decision"
|
|
14111
14292
|
};
|
|
14112
|
-
/**
|
|
14293
|
+
/** True for the measures that count risks, issues or decisions rather than tasks. */
|
|
14294
|
+
const isRaidMeasure = (m) => m in RAID_KIND;
|
|
14295
|
+
/** The splits a measure can take: hours have no status or priority of their own, risks no project fields. */
|
|
14113
14296
|
function groupsFor(measure) {
|
|
14114
|
-
|
|
14297
|
+
if (measure === "hours") return [
|
|
14298
|
+
"owner",
|
|
14299
|
+
"project",
|
|
14300
|
+
"program",
|
|
14301
|
+
"week",
|
|
14302
|
+
"field"
|
|
14303
|
+
];
|
|
14304
|
+
if (measure === "decisions") return [
|
|
14305
|
+
"owner",
|
|
14306
|
+
"project",
|
|
14307
|
+
"program",
|
|
14308
|
+
"week"
|
|
14309
|
+
];
|
|
14310
|
+
if (isRaidMeasure(measure)) return [
|
|
14311
|
+
"severity",
|
|
14115
14312
|
"owner",
|
|
14116
14313
|
"project",
|
|
14117
14314
|
"program",
|
|
14118
14315
|
"week"
|
|
14119
|
-
]
|
|
14316
|
+
];
|
|
14317
|
+
return [
|
|
14318
|
+
"status",
|
|
14319
|
+
"owner",
|
|
14320
|
+
"priority",
|
|
14321
|
+
"project",
|
|
14322
|
+
"program",
|
|
14323
|
+
"week",
|
|
14324
|
+
"field"
|
|
14325
|
+
];
|
|
14326
|
+
}
|
|
14327
|
+
/** What a chart can be kept to. Hours read the task they were logged on, so they take its priority and status. */
|
|
14328
|
+
function filtersFor(measure) {
|
|
14329
|
+
if (measure === "decisions") return ["owner"];
|
|
14330
|
+
if (isRaidMeasure(measure)) return ["severity", "owner"];
|
|
14331
|
+
return [
|
|
14332
|
+
"priority",
|
|
14333
|
+
"owner",
|
|
14334
|
+
"status",
|
|
14335
|
+
"field"
|
|
14336
|
+
];
|
|
14337
|
+
}
|
|
14338
|
+
/** "Owner" means who logged the hours, and who's asked to approve; the editor says so. */
|
|
14339
|
+
function groupLabel(measure, group) {
|
|
14340
|
+
if (group === "owner" && measure === "hours") return "Who logged it";
|
|
14341
|
+
if (group === "owner" && measure === "approval") return "Approver";
|
|
14342
|
+
return GROUP_LABEL[group];
|
|
14120
14343
|
}
|
|
14121
14344
|
const STATUS_FILL = {
|
|
14122
14345
|
backlog: "var(--color-swatch-slate)",
|
|
@@ -14137,6 +14360,21 @@ const PRIORITIES$1 = [
|
|
|
14137
14360
|
"medium",
|
|
14138
14361
|
"low"
|
|
14139
14362
|
];
|
|
14363
|
+
const SEVERITIES = [
|
|
14364
|
+
"high",
|
|
14365
|
+
"medium",
|
|
14366
|
+
"low"
|
|
14367
|
+
];
|
|
14368
|
+
const SEVERITY_LABEL = {
|
|
14369
|
+
high: "High",
|
|
14370
|
+
medium: "Medium",
|
|
14371
|
+
low: "Low"
|
|
14372
|
+
};
|
|
14373
|
+
const SEVERITY_FILL = {
|
|
14374
|
+
high: "var(--color-swatch-coral)",
|
|
14375
|
+
medium: "var(--color-swatch-sand)",
|
|
14376
|
+
low: "var(--color-swatch-slate)"
|
|
14377
|
+
};
|
|
14140
14378
|
/** Colours handed out in turn where a slice has none of its own. */
|
|
14141
14379
|
const CYCLE = [
|
|
14142
14380
|
"sea",
|
|
@@ -14158,14 +14396,21 @@ function mondayOf(day) {
|
|
|
14158
14396
|
}
|
|
14159
14397
|
/** The projects a chart reads: everything, or what sits under its scope. */
|
|
14160
14398
|
function scopeOf(data, scopeId) {
|
|
14399
|
+
const none = {
|
|
14400
|
+
programIds: null,
|
|
14401
|
+
portfolioId: null
|
|
14402
|
+
};
|
|
14161
14403
|
if (!scopeId) return {
|
|
14162
14404
|
projectIds: null,
|
|
14405
|
+
...none,
|
|
14163
14406
|
name: "Whole harbor",
|
|
14164
14407
|
found: true
|
|
14165
14408
|
};
|
|
14166
14409
|
const project = byId(data.projects).get(scopeId);
|
|
14167
14410
|
if (project) return {
|
|
14168
14411
|
projectIds: /* @__PURE__ */ new Set([project.id]),
|
|
14412
|
+
programIds: /* @__PURE__ */ new Set(),
|
|
14413
|
+
portfolioId: null,
|
|
14169
14414
|
name: project.name,
|
|
14170
14415
|
found: true
|
|
14171
14416
|
};
|
|
@@ -14174,6 +14419,8 @@ function scopeOf(data, scopeId) {
|
|
|
14174
14419
|
const ids = data.projects.filter((p) => p.programId === program.id).map((p) => p.id);
|
|
14175
14420
|
return {
|
|
14176
14421
|
projectIds: new Set(ids),
|
|
14422
|
+
programIds: /* @__PURE__ */ new Set([program.id]),
|
|
14423
|
+
portfolioId: null,
|
|
14177
14424
|
name: program.name,
|
|
14178
14425
|
found: true
|
|
14179
14426
|
};
|
|
@@ -14184,60 +14431,243 @@ function scopeOf(data, scopeId) {
|
|
|
14184
14431
|
const ids = data.projects.filter((p) => programIds.has(p.programId)).map((p) => p.id);
|
|
14185
14432
|
return {
|
|
14186
14433
|
projectIds: new Set(ids),
|
|
14434
|
+
programIds,
|
|
14435
|
+
portfolioId: portfolio.id,
|
|
14187
14436
|
name: portfolio.name,
|
|
14188
14437
|
found: true
|
|
14189
14438
|
};
|
|
14190
14439
|
}
|
|
14191
14440
|
return {
|
|
14192
14441
|
projectIds: /* @__PURE__ */ new Set(),
|
|
14442
|
+
programIds: /* @__PURE__ */ new Set(),
|
|
14443
|
+
portfolioId: null,
|
|
14193
14444
|
name: "Deleted",
|
|
14194
14445
|
found: false
|
|
14195
14446
|
};
|
|
14196
14447
|
}
|
|
14448
|
+
/** A risk, issue or decision belongs to a scope when it was raised in it or anywhere under it. */
|
|
14449
|
+
function raidInScope(item, scope) {
|
|
14450
|
+
if (!scope.projectIds) return true;
|
|
14451
|
+
if (item.projectId) return scope.projectIds.has(item.projectId);
|
|
14452
|
+
if (item.programId) return scope.programIds?.has(item.programId) ?? false;
|
|
14453
|
+
return !!item.portfolioId && item.portfolioId === scope.portfolioId;
|
|
14454
|
+
}
|
|
14455
|
+
const isGroupable = (def) => def.type === "select" || def.type === "multi" || def.type === "person" || def.type === "checkbox";
|
|
14456
|
+
function matchesFilter(def, value, want) {
|
|
14457
|
+
if (!want) return !hasFieldValue(def, value);
|
|
14458
|
+
if (def.type === "checkbox") return value === true;
|
|
14459
|
+
if (def.type === "multi") return tagsOf(value).includes(want);
|
|
14460
|
+
return value === want;
|
|
14461
|
+
}
|
|
14462
|
+
const sameName = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
14197
14463
|
/**
|
|
14198
|
-
*
|
|
14199
|
-
*
|
|
14200
|
-
*
|
|
14464
|
+
* Each project's field of this name, where a chart can split or filter by it:
|
|
14465
|
+
* the kind of field that has a short list of values (choice, tags, person,
|
|
14466
|
+
* checkbox), and the same kind as the first project that has one, so "Team"
|
|
14467
|
+
* as a choice in one project and a person in another doesn't mix the two.
|
|
14468
|
+
*/
|
|
14469
|
+
function fieldDefs(projects, name) {
|
|
14470
|
+
const defs = /* @__PURE__ */ new Map();
|
|
14471
|
+
if (!name?.trim()) return defs;
|
|
14472
|
+
let type = null;
|
|
14473
|
+
for (const p of projects) {
|
|
14474
|
+
const def = (p.customFields ?? []).find((d) => sameName(d.name, name) && isGroupable(d));
|
|
14475
|
+
if (!def) continue;
|
|
14476
|
+
type ??= def.type;
|
|
14477
|
+
if (def.type === type) defs.set(p.id, def);
|
|
14478
|
+
}
|
|
14479
|
+
return defs;
|
|
14480
|
+
}
|
|
14481
|
+
/** The fields a chart over this scope can split or filter by, by name, in the projects' own order. */
|
|
14482
|
+
function chartFields(data, scopeId) {
|
|
14483
|
+
const scope = scopeOf(data, scopeId);
|
|
14484
|
+
const out = [];
|
|
14485
|
+
for (const p of data.projects) {
|
|
14486
|
+
if (scope.projectIds && !scope.projectIds.has(p.id)) continue;
|
|
14487
|
+
for (const def of p.customFields ?? []) {
|
|
14488
|
+
if (!isGroupable(def) || out.some((f) => sameName(f.name, def.name))) continue;
|
|
14489
|
+
out.push({
|
|
14490
|
+
name: def.name.trim(),
|
|
14491
|
+
def
|
|
14492
|
+
});
|
|
14493
|
+
}
|
|
14494
|
+
}
|
|
14495
|
+
return out;
|
|
14496
|
+
}
|
|
14497
|
+
/** The values a filter can keep to, with "" meaning none ("Nobody", "No Size"). */
|
|
14498
|
+
function filterValues(measure, filter, data, scopeId) {
|
|
14499
|
+
switch (filter.by) {
|
|
14500
|
+
case "priority": return PRIORITIES$1.map((p) => ({
|
|
14501
|
+
value: p,
|
|
14502
|
+
label: PRIORITY_LABEL[p]
|
|
14503
|
+
}));
|
|
14504
|
+
case "status": return TASK_COLUMNS.map((s) => ({
|
|
14505
|
+
value: s,
|
|
14506
|
+
label: TASK_STATUS_LABEL[s]
|
|
14507
|
+
}));
|
|
14508
|
+
case "severity": return SEVERITIES.map((s) => ({
|
|
14509
|
+
value: s,
|
|
14510
|
+
label: SEVERITY_LABEL[s]
|
|
14511
|
+
}));
|
|
14512
|
+
case "owner": return [...data.people.map((p) => ({
|
|
14513
|
+
value: p.id,
|
|
14514
|
+
label: p.name
|
|
14515
|
+
})), {
|
|
14516
|
+
value: "",
|
|
14517
|
+
label: measure === "approval" ? "No approver" : "Nobody"
|
|
14518
|
+
}];
|
|
14519
|
+
case "field": {
|
|
14520
|
+
const found = chartFields(data, scopeId).find((f) => sameName(f.name, filter.field ?? ""));
|
|
14521
|
+
if (!found) return [];
|
|
14522
|
+
const def = {
|
|
14523
|
+
...found.def,
|
|
14524
|
+
name: found.name
|
|
14525
|
+
};
|
|
14526
|
+
if (def.type === "select" || def.type === "multi") {
|
|
14527
|
+
const scope = scopeOf(data, scopeId);
|
|
14528
|
+
const inScope = data.projects.filter((p) => !scope.projectIds || scope.projectIds.has(p.id));
|
|
14529
|
+
const options = /* @__PURE__ */ new Set();
|
|
14530
|
+
for (const d of fieldDefs(inScope, def.name).values()) for (const o of d.options ?? []) options.add(o);
|
|
14531
|
+
return [...[...options].map((o) => ({
|
|
14532
|
+
value: o,
|
|
14533
|
+
label: o
|
|
14534
|
+
})), {
|
|
14535
|
+
value: "",
|
|
14536
|
+
label: `No ${def.name}`
|
|
14537
|
+
}];
|
|
14538
|
+
}
|
|
14539
|
+
return def.type === "checkbox" ? [{
|
|
14540
|
+
value: "yes",
|
|
14541
|
+
label: def.name
|
|
14542
|
+
}, {
|
|
14543
|
+
value: "",
|
|
14544
|
+
label: `Not ${def.name}`
|
|
14545
|
+
}] : [...data.people.map((p) => ({
|
|
14546
|
+
value: p.id,
|
|
14547
|
+
label: p.name
|
|
14548
|
+
})), {
|
|
14549
|
+
value: "",
|
|
14550
|
+
label: `No ${def.name}`
|
|
14551
|
+
}];
|
|
14552
|
+
}
|
|
14553
|
+
}
|
|
14554
|
+
}
|
|
14555
|
+
/** "Only urgent", "Only Ana's", "Only Size: L", "Only no owner". */
|
|
14556
|
+
function filterText(measure, filter, data, scopeId) {
|
|
14557
|
+
const personField = filter.by === "field" && chartFields(data, scopeId).find((f) => sameName(f.name, filter.field ?? ""))?.def.type === "person";
|
|
14558
|
+
const label = filterValues(measure, filter, data, scopeId).find((c) => c.value === filter.value)?.label ?? (filter.by === "owner" ? "Former member" : personField ? "Someone no longer here" : filter.value);
|
|
14559
|
+
if (filter.by === "field") {
|
|
14560
|
+
const name = filter.field?.trim() ?? "";
|
|
14561
|
+
if (!filter.value) return `Only ${label.charAt(0).toLowerCase()}${label.slice(1)}`;
|
|
14562
|
+
return sameName(label, name) ? `Only ${label}` : `Only ${name}: ${label}`;
|
|
14563
|
+
}
|
|
14564
|
+
if (filter.by === "owner") {
|
|
14565
|
+
if (!filter.value) return measure === "approval" ? "Only with no approver" : "Only with no owner";
|
|
14566
|
+
if (measure === "hours") return `Only logged by ${label}`;
|
|
14567
|
+
if (measure === "approval") return `Only for ${label} to approve`;
|
|
14568
|
+
return `Only ${label}'s`;
|
|
14569
|
+
}
|
|
14570
|
+
if (filter.by === "severity") return `Only ${label.toLowerCase()} severity`;
|
|
14571
|
+
return `Only ${label.toLowerCase()}`;
|
|
14572
|
+
}
|
|
14573
|
+
/**
|
|
14574
|
+
* What a chart shows today: its slices in a fixed order (statuses,
|
|
14575
|
+
* priorities and severities in their own order, weeks in time order, the
|
|
14576
|
+
* rest largest first, past MAX_BARS summed into "Everything else"), with
|
|
14577
|
+
* their total.
|
|
14201
14578
|
*/
|
|
14202
14579
|
function chartData(chart, data, today) {
|
|
14580
|
+
const measure = MEASURES.includes(chart.measure) ? chart.measure : "open";
|
|
14203
14581
|
const scope = scopeOf(data, chart.scopeId);
|
|
14204
14582
|
const inScope = (t) => !scope.projectIds || scope.projectIds.has(t.projectId);
|
|
14205
|
-
const
|
|
14206
|
-
const
|
|
14583
|
+
const kind = RAID_KIND[measure];
|
|
14584
|
+
const unit = kind ? measure : measure === "hours" ? "hours" : "tasks";
|
|
14585
|
+
const groupBy = groupsFor(measure).includes(chart.groupBy) ? chart.groupBy : "owner";
|
|
14586
|
+
const filter = chart.filter && filtersFor(measure).includes(chart.filter.by) ? chart.filter : null;
|
|
14587
|
+
const projectsInScope = scope.projectIds ? data.projects.filter((p) => scope.projectIds.has(p.id)) : data.projects;
|
|
14588
|
+
const splitDefs = groupBy === "field" ? fieldDefs(projectsInScope, chart.field) : null;
|
|
14589
|
+
const filterDefs = filter?.by === "field" ? fieldDefs(projectsInScope, filter.field) : null;
|
|
14590
|
+
const tasksById = measure === "blocked" ? byId(data.tasks) : null;
|
|
14591
|
+
const keep = (item) => {
|
|
14592
|
+
if (!filter) return true;
|
|
14593
|
+
const t = item.task;
|
|
14594
|
+
switch (filter.by) {
|
|
14595
|
+
case "owner": return (item.personId ?? "") === filter.value;
|
|
14596
|
+
case "priority": return !!t && t.priority === filter.value;
|
|
14597
|
+
case "status": return !!t && t.status === filter.value;
|
|
14598
|
+
case "severity": return !!item.raid && item.raid.severity === filter.value;
|
|
14599
|
+
case "field": {
|
|
14600
|
+
if (!t) return false;
|
|
14601
|
+
const def = filterDefs.get(t.projectId);
|
|
14602
|
+
if (!def) return filter.value === "";
|
|
14603
|
+
return matchesFilter(def, t.customFieldValues?.[def.id], filter.value);
|
|
14604
|
+
}
|
|
14605
|
+
}
|
|
14606
|
+
};
|
|
14207
14607
|
const items = [];
|
|
14208
|
-
|
|
14608
|
+
const push = (item) => {
|
|
14609
|
+
if (keep(item)) items.push(item);
|
|
14610
|
+
};
|
|
14611
|
+
if (kind) for (const r of data.raidItems ?? []) {
|
|
14612
|
+
if (r.kind !== kind || !ACTIVE_RAID.has(r.status) || !raidInScope(r, scope)) continue;
|
|
14613
|
+
push({
|
|
14614
|
+
raid: r,
|
|
14615
|
+
amount: 1,
|
|
14616
|
+
day: r.dueDate,
|
|
14617
|
+
personId: r.ownerId
|
|
14618
|
+
});
|
|
14619
|
+
}
|
|
14620
|
+
else for (const task of data.tasks) {
|
|
14209
14621
|
if (!inScope(task)) continue;
|
|
14210
14622
|
const done = task.status === "done";
|
|
14211
|
-
|
|
14623
|
+
const mine = {
|
|
14624
|
+
task,
|
|
14625
|
+
amount: 1,
|
|
14626
|
+
personId: task.assigneeId
|
|
14627
|
+
};
|
|
14628
|
+
switch (measure) {
|
|
14212
14629
|
case "open":
|
|
14213
|
-
if (!done)
|
|
14214
|
-
|
|
14215
|
-
amount: 1,
|
|
14630
|
+
if (!done) push({
|
|
14631
|
+
...mine,
|
|
14216
14632
|
day: task.dueDate
|
|
14217
14633
|
});
|
|
14218
14634
|
break;
|
|
14219
14635
|
case "done":
|
|
14220
|
-
if (done)
|
|
14221
|
-
|
|
14222
|
-
amount: 1,
|
|
14636
|
+
if (done) push({
|
|
14637
|
+
...mine,
|
|
14223
14638
|
day: task.completedAt
|
|
14224
14639
|
});
|
|
14225
14640
|
break;
|
|
14226
14641
|
case "all":
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
amount: 1,
|
|
14642
|
+
push({
|
|
14643
|
+
...mine,
|
|
14230
14644
|
day: task.createdAt
|
|
14231
14645
|
});
|
|
14232
14646
|
break;
|
|
14233
14647
|
case "overdue":
|
|
14234
|
-
if (!done && task.dueDate && task.dueDate.slice(0, 10) < today)
|
|
14648
|
+
if (!done && task.dueDate && task.dueDate.slice(0, 10) < today) push({
|
|
14649
|
+
...mine,
|
|
14650
|
+
day: task.dueDate
|
|
14651
|
+
});
|
|
14652
|
+
break;
|
|
14653
|
+
case "blocked":
|
|
14654
|
+
if (!done && blockerIds(task).some((id) => {
|
|
14655
|
+
const b = tasksById.get(id);
|
|
14656
|
+
return !!b && b.status !== "done";
|
|
14657
|
+
})) push({
|
|
14658
|
+
...mine,
|
|
14659
|
+
day: task.dueDate
|
|
14660
|
+
});
|
|
14661
|
+
break;
|
|
14662
|
+
case "approval":
|
|
14663
|
+
if (task.approval?.status === "pending") push({
|
|
14235
14664
|
task,
|
|
14236
14665
|
amount: 1,
|
|
14237
|
-
day: task.
|
|
14666
|
+
day: task.approval.askedAt,
|
|
14667
|
+
personId: task.approval.approverId
|
|
14238
14668
|
});
|
|
14239
14669
|
break;
|
|
14240
|
-
case "hours": for (const entry of task.time ?? [])
|
|
14670
|
+
case "hours": for (const entry of task.time ?? []) push({
|
|
14241
14671
|
task,
|
|
14242
14672
|
amount: entry.minutes / 60,
|
|
14243
14673
|
day: entry.on,
|
|
@@ -14250,10 +14680,14 @@ function chartData(chart, data, today) {
|
|
|
14250
14680
|
const people = byId(data.people);
|
|
14251
14681
|
const sums = /* @__PURE__ */ new Map();
|
|
14252
14682
|
const add = (key, amount) => sums.set(key, (sums.get(key) ?? 0) + amount);
|
|
14683
|
+
/** Field values in the order the field lists them, for a field split. */
|
|
14684
|
+
const fieldLabels = /* @__PURE__ */ new Map();
|
|
14685
|
+
let fieldNone = "";
|
|
14686
|
+
let overlaps = false;
|
|
14253
14687
|
let weeks = [];
|
|
14254
14688
|
if (groupBy === "week") {
|
|
14255
14689
|
const thisWeek = mondayOf(today);
|
|
14256
|
-
const ahead =
|
|
14690
|
+
const ahead = measure !== "done" && measure !== "all" && measure !== "overdue" && measure !== "approval" && measure !== "hours";
|
|
14257
14691
|
const first = ahead ? thisWeek : format(addDays(parseIso(`${thisWeek}T12:00:00`), -77), "yyyy-MM-dd");
|
|
14258
14692
|
const count = ahead ? 8 : 12;
|
|
14259
14693
|
const start = parseIso(`${first}T12:00:00`);
|
|
@@ -14265,9 +14699,36 @@ function chartData(chart, data, today) {
|
|
|
14265
14699
|
if (known.has(week)) add(week, item.amount);
|
|
14266
14700
|
else if (ahead && week < first) add(first, item.amount);
|
|
14267
14701
|
}
|
|
14702
|
+
} else if (groupBy === "field") {
|
|
14703
|
+
const name = chart.field?.trim() || "field";
|
|
14704
|
+
const first = splitDefs.values().next().value;
|
|
14705
|
+
const shown = first?.name.trim() || name;
|
|
14706
|
+
fieldNone = first?.type === "checkbox" ? `Not ${shown}` : `No ${shown}`;
|
|
14707
|
+
for (const item of items) {
|
|
14708
|
+
const t = item.task;
|
|
14709
|
+
const def = splitDefs.get(t.projectId);
|
|
14710
|
+
const v = def ? t.customFieldValues?.[def.id] : void 0;
|
|
14711
|
+
if (!def || !hasFieldValue(def, v)) add("", item.amount);
|
|
14712
|
+
else if (def.type === "multi") {
|
|
14713
|
+
const tags = [...new Set(tagsOf(v))];
|
|
14714
|
+
if (tags.length > 1) overlaps = true;
|
|
14715
|
+
for (const tag of tags) {
|
|
14716
|
+
fieldLabels.set(tag, tag);
|
|
14717
|
+
add(tag, item.amount);
|
|
14718
|
+
}
|
|
14719
|
+
} else if (def.type === "checkbox") {
|
|
14720
|
+
fieldLabels.set("yes", def.name.trim());
|
|
14721
|
+
add("yes", item.amount);
|
|
14722
|
+
} else {
|
|
14723
|
+
const key = String(v);
|
|
14724
|
+
fieldLabels.set(key, fieldValueText(def, v, (id) => people.get(id)?.name) || key);
|
|
14725
|
+
add(key, item.amount);
|
|
14726
|
+
}
|
|
14727
|
+
}
|
|
14268
14728
|
} else for (const item of items) {
|
|
14269
14729
|
const t = item.task;
|
|
14270
|
-
|
|
14730
|
+
const r = item.raid;
|
|
14731
|
+
add(groupBy === "status" ? t?.status ?? "" : groupBy === "priority" ? t?.priority ?? "" : groupBy === "severity" ? r?.severity ?? "" : groupBy === "project" ? (t ? t.projectId : r?.projectId) ?? "" : groupBy === "program" ? t ? projects.get(t.projectId)?.programId ?? "" : r?.programId ?? (r?.projectId ? projects.get(r.projectId)?.programId : null) ?? "" : item.personId ?? "", item.amount);
|
|
14271
14732
|
}
|
|
14272
14733
|
const round = (n) => unit === "hours" ? Math.round(n * 10) / 10 : n;
|
|
14273
14734
|
let slices;
|
|
@@ -14289,20 +14750,38 @@ function chartData(chart, data, today) {
|
|
|
14289
14750
|
value: round(sums.get(p)),
|
|
14290
14751
|
fill: PRIORITY_FILL[p]
|
|
14291
14752
|
}));
|
|
14753
|
+
else if (groupBy === "severity") slices = SEVERITIES.filter((s) => sums.has(s)).map((s) => ({
|
|
14754
|
+
key: s,
|
|
14755
|
+
label: SEVERITY_LABEL[s],
|
|
14756
|
+
value: round(sums.get(s)),
|
|
14757
|
+
fill: SEVERITY_FILL[s]
|
|
14758
|
+
}));
|
|
14292
14759
|
else {
|
|
14293
14760
|
const named = [...sums.entries()].map(([key, value], i) => {
|
|
14761
|
+
if (groupBy === "field") return {
|
|
14762
|
+
key: key || "none",
|
|
14763
|
+
label: key ? fieldLabels.get(key) ?? key : fieldNone,
|
|
14764
|
+
value: round(value),
|
|
14765
|
+
fill: key ? swatchFill(CYCLE[i % CYCLE.length]) : "var(--color-swatch-slate)"
|
|
14766
|
+
};
|
|
14294
14767
|
if (groupBy === "owner") {
|
|
14295
14768
|
const person = key ? people.get(key) : void 0;
|
|
14296
14769
|
return {
|
|
14297
14770
|
key: key || "nobody",
|
|
14298
|
-
label: key ? person?.name ?? "Former member" : "Nobody",
|
|
14771
|
+
label: key ? person?.name ?? "Former member" : measure === "approval" ? "No approver" : "Nobody",
|
|
14299
14772
|
value: round(value),
|
|
14300
14773
|
fill: key ? swatchFill(CYCLE[i % CYCLE.length]) : "var(--color-swatch-slate)"
|
|
14301
14774
|
};
|
|
14302
14775
|
}
|
|
14776
|
+
if (!key) return {
|
|
14777
|
+
key: "none",
|
|
14778
|
+
label: groupBy === "project" ? "Not on a project" : "Not in a program",
|
|
14779
|
+
value: round(value),
|
|
14780
|
+
fill: "var(--color-swatch-slate)"
|
|
14781
|
+
};
|
|
14303
14782
|
const thing = groupBy === "project" ? projects.get(key) : programs.get(key);
|
|
14304
14783
|
return {
|
|
14305
|
-
key
|
|
14784
|
+
key,
|
|
14306
14785
|
label: thing?.name ?? "Deleted",
|
|
14307
14786
|
value: round(value),
|
|
14308
14787
|
fill: thing?.swatch ? swatchFill(thing.swatch) : swatchFill(CYCLE[i % CYCLE.length])
|
|
@@ -14319,24 +14798,37 @@ function chartData(chart, data, today) {
|
|
|
14319
14798
|
}];
|
|
14320
14799
|
} else slices = named;
|
|
14321
14800
|
}
|
|
14322
|
-
const total = round(slices.reduce((n, s) => n + s.value, 0));
|
|
14801
|
+
const total = overlaps ? round(items.reduce((n, i) => n + i.amount, 0)) : round(slices.reduce((n, s) => n + s.value, 0));
|
|
14323
14802
|
return {
|
|
14324
14803
|
slices,
|
|
14325
14804
|
total,
|
|
14326
14805
|
unit,
|
|
14327
14806
|
scopeName: scope.name,
|
|
14328
|
-
scopeFound: scope.found
|
|
14807
|
+
scopeFound: scope.found,
|
|
14808
|
+
filterText: filter ? filterText(measure, filter, data, chart.scopeId) : null,
|
|
14809
|
+
overlaps
|
|
14329
14810
|
};
|
|
14330
14811
|
}
|
|
14331
|
-
|
|
14812
|
+
const UNIT_WORDS = {
|
|
14813
|
+
tasks: ["task", "tasks"],
|
|
14814
|
+
hours: ["hour", "hours"],
|
|
14815
|
+
risks: ["risk", "risks"],
|
|
14816
|
+
issues: ["issue", "issues"],
|
|
14817
|
+
decisions: ["decision", "decisions"]
|
|
14818
|
+
};
|
|
14819
|
+
/** "task" or "tasks", "risk" or "risks". */
|
|
14820
|
+
function unitWord(value, unit) {
|
|
14821
|
+
return UNIT_WORDS[unit][value === 1 ? 0 : 1];
|
|
14822
|
+
}
|
|
14823
|
+
/** "12 tasks", "1 task", "7.5 hours", "3 risks". */
|
|
14332
14824
|
function amountText(value, unit) {
|
|
14333
|
-
|
|
14334
|
-
return `${value} ${value === 1 ? "task" : "tasks"}`;
|
|
14825
|
+
return `${value} ${unitWord(value, unit)}`;
|
|
14335
14826
|
}
|
|
14336
14827
|
/** A chart's text for screen readers, agents and the terminal: "Open tasks by owner: Sam 4, Ana 2 (6 tasks)". */
|
|
14337
14828
|
function chartText(title, result) {
|
|
14338
|
-
|
|
14339
|
-
|
|
14829
|
+
const named = result.filterText ? `${title} (${result.filterText.toLowerCase()})` : title;
|
|
14830
|
+
if (result.slices.length === 0) return `${named}: nothing yet`;
|
|
14831
|
+
return `${named}: ${result.slices.map((s) => `${s.label} ${s.value}`).join(", ")} (${amountText(result.total, result.unit)})`;
|
|
14340
14832
|
}
|
|
14341
14833
|
|
|
14342
14834
|
// node_modules/zod/v4/core/util.js
|
|
@@ -19918,85 +20410,6 @@ function exportName(harborName, now) {
|
|
|
19918
20410
|
return `${harborName.normalize("NFKD").replace(/\p{M}/gu, "").replace(/&/g, " and ").replace(/[^\w]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "harbor"}-${now.toISOString().slice(0, 10)}`;
|
|
19919
20411
|
}
|
|
19920
20412
|
|
|
19921
|
-
// src/lib/custom-field-text.ts
|
|
19922
|
-
init_date_text();
|
|
19923
|
-
const FIELD_TYPE_LABEL = {
|
|
19924
|
-
text: "Text",
|
|
19925
|
-
number: "Number",
|
|
19926
|
-
select: "Dropdown",
|
|
19927
|
-
multi: "Tags",
|
|
19928
|
-
checkbox: "Checkbox",
|
|
19929
|
-
date: "Date",
|
|
19930
|
-
person: "Person",
|
|
19931
|
-
url: "Link"
|
|
19932
|
-
};
|
|
19933
|
-
const FIELD_TYPES = Object.keys(FIELD_TYPE_LABEL);
|
|
19934
|
-
/** A dropdown or tags field: one with a list of options. */
|
|
19935
|
-
function hasOptions(type) {
|
|
19936
|
-
return type === "select" || type === "multi";
|
|
19937
|
-
}
|
|
19938
|
-
/** A dropdown's options from "S, M, L". */
|
|
19939
|
-
function optionsOf(value) {
|
|
19940
|
-
return [...new Set(value.split(",").map((o) => o.trim()).filter(Boolean))];
|
|
19941
|
-
}
|
|
19942
|
-
/** The link a value names when it's an http(s) address, else null (never javascript: and the like). */
|
|
19943
|
-
function safeLink(value) {
|
|
19944
|
-
if (typeof value !== "string") return null;
|
|
19945
|
-
try {
|
|
19946
|
-
const u = new URL(value.trim());
|
|
19947
|
-
return u.protocol === "https:" || u.protocol === "http:" ? u.href : null;
|
|
19948
|
-
} catch {
|
|
19949
|
-
return null;
|
|
19950
|
-
}
|
|
19951
|
-
}
|
|
19952
|
-
/** A link from "example.com/x" or "https://…"; null when it isn't a web address. */
|
|
19953
|
-
function readLink(text) {
|
|
19954
|
-
const t = text.trim();
|
|
19955
|
-
if (!t || /\s/.test(t)) return null;
|
|
19956
|
-
const href = safeLink(/^[a-z][a-z0-9+.-]*:/i.test(t) ? t : `https://${t}`);
|
|
19957
|
-
return href && /\./.test(new URL(href).hostname) ? href : null;
|
|
19958
|
-
}
|
|
19959
|
-
/** "example.com/docs/plan" from a link: what a short chip says. */
|
|
19960
|
-
function linkLabel(href) {
|
|
19961
|
-
try {
|
|
19962
|
-
const u = new URL(href);
|
|
19963
|
-
const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
|
|
19964
|
-
const text = u.host.replace(/^www\./, "") + path;
|
|
19965
|
-
return text.length > 40 ? `${text.slice(0, 39)}…` : text;
|
|
19966
|
-
} catch {
|
|
19967
|
-
return href;
|
|
19968
|
-
}
|
|
19969
|
-
}
|
|
19970
|
-
/** A tags field's value as a list, whatever was stored. */
|
|
19971
|
-
function tagsOf(value) {
|
|
19972
|
-
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
19973
|
-
return typeof value === "string" && value ? [value] : [];
|
|
19974
|
-
}
|
|
19975
|
-
/** Whether a task carries a value for the field. */
|
|
19976
|
-
function hasFieldValue(def, value) {
|
|
19977
|
-
if (value == null || value === "" || value === false) return false;
|
|
19978
|
-
if (def.type === "multi") return tagsOf(value).length > 0;
|
|
19979
|
-
return true;
|
|
19980
|
-
}
|
|
19981
|
-
/** A task's value for a field as one line: "" when it has none. */
|
|
19982
|
-
function fieldValueText(def, value, nameOf) {
|
|
19983
|
-
if (!hasFieldValue(def, value)) return "";
|
|
19984
|
-
switch (def.type) {
|
|
19985
|
-
case "checkbox": return value ? "Yes" : "";
|
|
19986
|
-
case "multi": return tagsOf(value).join(", ");
|
|
19987
|
-
case "date": {
|
|
19988
|
-
const d = parseIso(String(value));
|
|
19989
|
-
return Number.isNaN(d.getTime()) ? String(value) : format(d, "MMM d, yyyy");
|
|
19990
|
-
}
|
|
19991
|
-
case "person": return nameOf?.(String(value)) ?? "Someone no longer here";
|
|
19992
|
-
case "url": {
|
|
19993
|
-
const href = safeLink(value);
|
|
19994
|
-
return href ? linkLabel(href) : "";
|
|
19995
|
-
}
|
|
19996
|
-
default: return Array.isArray(value) ? value.join(", ") : String(value);
|
|
19997
|
-
}
|
|
19998
|
-
}
|
|
19999
|
-
|
|
20000
20413
|
// src/lib/exports.ts
|
|
20001
20414
|
init_subtasks();
|
|
20002
20415
|
init_derived_health();
|
|
@@ -22169,7 +22582,7 @@ const HARBOR_TOOLS = [
|
|
|
22169
22582
|
tool({
|
|
22170
22583
|
name: "list_dashboards",
|
|
22171
22584
|
title: "Dashboards",
|
|
22172
|
-
description: "The harbor's dashboards from the Reports page, each chart with what it counts (open, finished, overdue or all tasks, or
|
|
22585
|
+
description: "The harbor's dashboards from the Reports page, each chart with what it counts (open, finished, overdue, blocked, waiting-for-approval or all tasks, hours logged, open risks, open issues or decisions to make), how it's split (status, owner, priority, project, program, week, severity or one of the projects' own fields by name), what it's kept to (only: one priority, owner, status, severity or field value; absent when it counts everything), the part of the harbor it reads, and its numbers as of today, with a one-line text of each. Subtasks count as tasks of their own; with a tags field one task can sit under two values, so the values can add up to more than the total." + DATA_NOTE,
|
|
22173
22586
|
input: object({
|
|
22174
22587
|
harbor: harborArg,
|
|
22175
22588
|
dashboard: string().max(200).optional().describe("Dashboard id or name. Default: all.")
|
|
@@ -22187,9 +22600,10 @@ const HARBOR_TOOLS = [
|
|
|
22187
22600
|
id: c.id,
|
|
22188
22601
|
title: c.title,
|
|
22189
22602
|
counts: MEASURE_LABEL[c.measure],
|
|
22190
|
-
splitBy:
|
|
22603
|
+
splitBy: c.groupBy === "field" ? c.field ?? "" : groupLabel(c.measure, c.groupBy),
|
|
22191
22604
|
view: c.view,
|
|
22192
22605
|
from: result.scopeName,
|
|
22606
|
+
...result.filterText ? { only: result.filterText } : {},
|
|
22193
22607
|
total: amountText(result.total, result.unit),
|
|
22194
22608
|
values: result.slices.map((s) => ({
|
|
22195
22609
|
label: s.label,
|
|
@@ -24652,7 +25066,7 @@ function recordScoreChange(item, patch, at) {
|
|
|
24652
25066
|
var init_raid_history = __esmMin((() => {}));
|
|
24653
25067
|
|
|
24654
25068
|
// src/lib/slices/milestones-raid.ts
|
|
24655
|
-
function milestonesRaidSlice({
|
|
25069
|
+
function milestonesRaidSlice({ get, write }) {
|
|
24656
25070
|
return {
|
|
24657
25071
|
toggleMilestone: (id) => {
|
|
24658
25072
|
if (get().milestones.find((m) => m.id === id)?.approverId) return;
|
|
@@ -24684,15 +25098,16 @@ function milestonesRaidSlice({ set, get, write }) {
|
|
|
24684
25098
|
write({ milestones: [...get().milestones, m] });
|
|
24685
25099
|
},
|
|
24686
25100
|
setConstraint: (programId, patch) => {
|
|
25101
|
+
const program = get().programs.find((p) => p.id === programId);
|
|
25102
|
+
const logNamed = () => get().log(`named the constraint on ${program?.name ?? "a program"}`, `/programs/${programId}`);
|
|
24687
25103
|
const existing = get().constraints.find((c) => c.programId === programId && c.status === "active");
|
|
24688
25104
|
if (existing) {
|
|
24689
|
-
|
|
25105
|
+
write({ constraints: get().constraints.map((c) => c.id === existing.id ? {
|
|
24690
25106
|
...c,
|
|
24691
25107
|
...patch,
|
|
24692
25108
|
status: "active"
|
|
24693
25109
|
} : c) });
|
|
24694
|
-
|
|
24695
|
-
get().log(`named the constraint on ${program?.name ?? "a program"}`, `/programs/${programId}`);
|
|
25110
|
+
logNamed();
|
|
24696
25111
|
return existing.id;
|
|
24697
25112
|
}
|
|
24698
25113
|
const id = uid("k");
|
|
@@ -24700,29 +25115,48 @@ function milestonesRaidSlice({ set, get, write }) {
|
|
|
24700
25115
|
...patch,
|
|
24701
25116
|
id,
|
|
24702
25117
|
programId,
|
|
24703
|
-
status: "active"
|
|
25118
|
+
status: "active",
|
|
25119
|
+
createdAt: stamp(),
|
|
25120
|
+
createdById: get().currentUserId || null
|
|
24704
25121
|
};
|
|
24705
|
-
|
|
24706
|
-
|
|
24707
|
-
get().log(`named the constraint on ${program?.name ?? "a program"}`, `/programs/${programId}`);
|
|
25122
|
+
write({ constraints: [...get().constraints, row] });
|
|
25123
|
+
logNamed();
|
|
24708
25124
|
return id;
|
|
24709
25125
|
},
|
|
24710
25126
|
updateConstraint: (id, patch) => {
|
|
24711
|
-
|
|
25127
|
+
write({ constraints: get().constraints.map((c) => c.id === id ? {
|
|
24712
25128
|
...c,
|
|
24713
25129
|
...patch
|
|
24714
25130
|
} : c) });
|
|
24715
25131
|
},
|
|
24716
25132
|
resolveConstraint: (id) => {
|
|
24717
25133
|
const row = get().constraints.find((c) => c.id === id);
|
|
24718
|
-
|
|
25134
|
+
if (!row || row.status === "resolved") return;
|
|
25135
|
+
write({ constraints: get().constraints.map((c) => c.id === id ? {
|
|
24719
25136
|
...c,
|
|
24720
|
-
status: "resolved"
|
|
25137
|
+
status: "resolved",
|
|
25138
|
+
resolvedAt: stamp(),
|
|
25139
|
+
resolvedById: get().currentUserId || null
|
|
24721
25140
|
} : c) });
|
|
24722
|
-
|
|
24723
|
-
|
|
24724
|
-
|
|
24725
|
-
|
|
25141
|
+
const program = get().programs.find((p) => p.id === row.programId);
|
|
25142
|
+
get().log(`retired the constraint on ${program?.name ?? "a program"}`, `/programs/${row.programId}`);
|
|
25143
|
+
},
|
|
25144
|
+
reopenConstraint: (id) => {
|
|
25145
|
+
const row = get().constraints.find((c) => c.id === id);
|
|
25146
|
+
if (!row) return false;
|
|
25147
|
+
if (row.status === "active") return true;
|
|
25148
|
+
if (get().constraints.some((c) => c.programId === row.programId && c.status === "active")) return false;
|
|
25149
|
+
write({ constraints: get().constraints.map((c) => {
|
|
25150
|
+
if (c.id !== id) return c;
|
|
25151
|
+
const { resolvedAt: _at, resolvedById: _by, ...rest } = c;
|
|
25152
|
+
return {
|
|
25153
|
+
...rest,
|
|
25154
|
+
status: "active"
|
|
25155
|
+
};
|
|
25156
|
+
}) });
|
|
25157
|
+
const program = get().programs.find((p) => p.id === row.programId);
|
|
25158
|
+
get().log(`brought back the constraint on ${program?.name ?? "a program"}`, `/programs/${row.programId}`);
|
|
25159
|
+
return true;
|
|
24726
25160
|
},
|
|
24727
25161
|
addRaidItem: (r) => {
|
|
24728
25162
|
const id = uid("raid");
|