@validation-os/dashboard 0.2.0 → 0.4.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/index.d.ts +292 -11
- package/dist/index.js +1218 -84
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -10,6 +10,15 @@ var REGISTER_LABEL = {
|
|
|
10
10
|
glossary: "Glossary",
|
|
11
11
|
people: "People"
|
|
12
12
|
};
|
|
13
|
+
var REGISTER_SINGULAR = {
|
|
14
|
+
assumptions: "Assumption",
|
|
15
|
+
experiments: "Experiment",
|
|
16
|
+
readings: "Reading",
|
|
17
|
+
goals: "Goal",
|
|
18
|
+
decisions: "Decision",
|
|
19
|
+
glossary: "Glossary term",
|
|
20
|
+
people: "Person"
|
|
21
|
+
};
|
|
13
22
|
var REGISTER_ORDER = [
|
|
14
23
|
"assumptions",
|
|
15
24
|
"experiments",
|
|
@@ -234,30 +243,29 @@ function RegisterTable({
|
|
|
234
243
|
}
|
|
235
244
|
|
|
236
245
|
// src/record-drawer.tsx
|
|
246
|
+
import { useEffect as useEffect3, useState as useState2 } from "react";
|
|
247
|
+
|
|
248
|
+
// src/drawer-shell.tsx
|
|
237
249
|
import { useEffect, useRef } from "react";
|
|
238
|
-
import {
|
|
239
|
-
|
|
240
|
-
function RecordDrawer({
|
|
241
|
-
register,
|
|
242
|
-
record,
|
|
243
|
-
loading,
|
|
244
|
-
error,
|
|
250
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
251
|
+
function DrawerShell({
|
|
245
252
|
open,
|
|
246
|
-
onClose
|
|
253
|
+
onClose,
|
|
254
|
+
ariaLabel,
|
|
255
|
+
scroll = true,
|
|
256
|
+
children
|
|
247
257
|
}) {
|
|
248
|
-
const
|
|
258
|
+
const panelRef = useRef(null);
|
|
249
259
|
useEffect(() => {
|
|
250
260
|
if (!open) return;
|
|
251
261
|
const onKey = (e) => {
|
|
252
262
|
if (e.key === "Escape") onClose();
|
|
253
263
|
};
|
|
254
264
|
document.addEventListener("keydown", onKey);
|
|
255
|
-
|
|
265
|
+
panelRef.current?.focus();
|
|
256
266
|
return () => document.removeEventListener("keydown", onKey);
|
|
257
267
|
}, [open, onClose]);
|
|
258
268
|
if (!open) return null;
|
|
259
|
-
const derived = record && record.derived && typeof record.derived === "object" ? record.derived : null;
|
|
260
|
-
const fields = record ? Object.keys(record).filter((k) => !META_FIELDS.has(k)) : [];
|
|
261
269
|
return /* @__PURE__ */ jsxs3("div", { className: "fixed inset-0 z-50 flex justify-end", children: [
|
|
262
270
|
/* @__PURE__ */ jsx3(
|
|
263
271
|
"button",
|
|
@@ -268,66 +276,134 @@ function RecordDrawer({
|
|
|
268
276
|
className: "absolute inset-0 bg-neutral-950/30 backdrop-blur-sm"
|
|
269
277
|
}
|
|
270
278
|
),
|
|
271
|
-
/* @__PURE__ */
|
|
272
|
-
"
|
|
279
|
+
/* @__PURE__ */ jsx3(
|
|
280
|
+
"div",
|
|
273
281
|
{
|
|
282
|
+
ref: panelRef,
|
|
274
283
|
role: "dialog",
|
|
275
284
|
"aria-modal": "true",
|
|
276
|
-
"aria-label":
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
/* @__PURE__ */ jsxs3("div", { children: [
|
|
281
|
-
/* @__PURE__ */ jsx3("p", { className: "text-xs font-medium uppercase tracking-wide text-neutral-400", children: REGISTER_LABEL[register] }),
|
|
282
|
-
/* @__PURE__ */ jsx3("h2", { className: "mt-1 text-lg font-semibold text-neutral-900 dark:text-neutral-50", children: record ? primaryLabel(record) : loading ? "Loading\u2026" : "\u2014" })
|
|
283
|
-
] }),
|
|
284
|
-
/* @__PURE__ */ jsx3(
|
|
285
|
-
"button",
|
|
286
|
-
{
|
|
287
|
-
ref: closeButtonRef,
|
|
288
|
-
type: "button",
|
|
289
|
-
onClick: onClose,
|
|
290
|
-
className: "rounded-md px-2 py-1 text-sm text-neutral-500 hover:bg-neutral-100 dark:hover:bg-neutral-900",
|
|
291
|
-
children: "Close"
|
|
292
|
-
}
|
|
293
|
-
)
|
|
294
|
-
] }),
|
|
295
|
-
/* @__PURE__ */ jsx3("div", { className: "flex-1 p-5", children: loading ? /* @__PURE__ */ jsx3("p", { className: "text-sm text-neutral-500", children: "Loading record\u2026" }) : error ? /* @__PURE__ */ jsx3("p", { className: "text-sm text-red-600 dark:text-red-400", children: error }) : !record ? /* @__PURE__ */ jsx3("p", { className: "text-sm text-neutral-500", children: "No record." }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
296
|
-
derived ? /* @__PURE__ */ jsxs3("section", { className: "mb-6", children: [
|
|
297
|
-
/* @__PURE__ */ jsx3("h3", { className: "mb-2 text-xs font-medium uppercase tracking-wide text-neutral-400", children: "Computed \xB7 not editable" }),
|
|
298
|
-
/* @__PURE__ */ jsx3("dl", { className: "grid grid-cols-3 gap-3", children: Object.entries(derived).map(([key, value]) => /* @__PURE__ */ jsxs3(
|
|
299
|
-
"div",
|
|
300
|
-
{
|
|
301
|
-
className: "rounded-lg border border-neutral-200 bg-neutral-50 p-3 dark:border-neutral-800 dark:bg-neutral-900",
|
|
302
|
-
children: [
|
|
303
|
-
/* @__PURE__ */ jsx3("dt", { className: "text-xs text-neutral-500 dark:text-neutral-400", children: derivedLabel(key) }),
|
|
304
|
-
/* @__PURE__ */ jsx3("dd", { className: "mt-1 text-xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-50", children: formatValue(value) })
|
|
305
|
-
]
|
|
306
|
-
},
|
|
307
|
-
key
|
|
308
|
-
)) })
|
|
309
|
-
] }) : null,
|
|
310
|
-
/* @__PURE__ */ jsx3("dl", { className: "divide-y divide-neutral-100 dark:divide-neutral-900", children: fields.map((key) => /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-3 gap-3 py-2.5", children: [
|
|
311
|
-
/* @__PURE__ */ jsx3("dt", { className: "text-sm text-neutral-500 dark:text-neutral-400", children: fieldLabel(key) }),
|
|
312
|
-
/* @__PURE__ */ jsx3("dd", { className: "col-span-2 text-sm text-neutral-800 dark:text-neutral-200", children: formatValue(record[key]) })
|
|
313
|
-
] }, key)) })
|
|
314
|
-
] }) }),
|
|
315
|
-
record ? /* @__PURE__ */ jsxs3("footer", { className: "border-t border-neutral-200 p-5 text-xs text-neutral-400 dark:border-neutral-800", children: [
|
|
316
|
-
record.id,
|
|
317
|
-
" \xB7 version ",
|
|
318
|
-
String(record.version),
|
|
319
|
-
" \xB7 updated",
|
|
320
|
-
" ",
|
|
321
|
-
formatValue(record.updatedAt)
|
|
322
|
-
] }) : null
|
|
323
|
-
]
|
|
285
|
+
"aria-label": ariaLabel,
|
|
286
|
+
tabIndex: -1,
|
|
287
|
+
className: `relative flex h-full w-full max-w-md flex-col border-l border-neutral-200 bg-white shadow-xl outline-none dark:border-neutral-800 dark:bg-neutral-950 ${scroll ? "overflow-y-auto" : "overflow-hidden"}`,
|
|
288
|
+
children
|
|
324
289
|
}
|
|
325
290
|
)
|
|
326
291
|
] });
|
|
327
292
|
}
|
|
328
293
|
|
|
329
|
-
// src/
|
|
330
|
-
|
|
294
|
+
// src/edit.ts
|
|
295
|
+
var CONFLICT_MESSAGE = "Someone edited this while you had it open \u2014 your changes are safe, take a look before saving again.";
|
|
296
|
+
var t = (key, label) => ({ key, label, kind: "text" });
|
|
297
|
+
var area = (key, label) => ({
|
|
298
|
+
key,
|
|
299
|
+
label,
|
|
300
|
+
kind: "textarea"
|
|
301
|
+
});
|
|
302
|
+
var num = (key, label) => ({
|
|
303
|
+
key,
|
|
304
|
+
label,
|
|
305
|
+
kind: "number",
|
|
306
|
+
nullable: true
|
|
307
|
+
});
|
|
308
|
+
var sel = (key, label, options, nullable = false) => ({ key, label, kind: "select", options, nullable });
|
|
309
|
+
var READING_RUNGS = [
|
|
310
|
+
"Opinion",
|
|
311
|
+
"Pitch-deck reaction",
|
|
312
|
+
"Anecdotal",
|
|
313
|
+
"Desk research",
|
|
314
|
+
"Survey at scale",
|
|
315
|
+
"Prototype usage",
|
|
316
|
+
"Signed intent",
|
|
317
|
+
"Paying users"
|
|
318
|
+
];
|
|
319
|
+
var SOURCE_QUALITY = ["1", "0.7", "0.5"];
|
|
320
|
+
var EDITORS = {
|
|
321
|
+
assumptions: [
|
|
322
|
+
t("Title", "Assumption"),
|
|
323
|
+
area("Description", "Description"),
|
|
324
|
+
num("Impact", "Impact"),
|
|
325
|
+
sel("Status", "Status", ["Draft", "Live", "Invalidated"]),
|
|
326
|
+
t("Lens", "Lens"),
|
|
327
|
+
area("5 Whys", "5 Whys"),
|
|
328
|
+
area("Metric for truth", "Metric for truth"),
|
|
329
|
+
area("Scoring justification", "Scoring justification")
|
|
330
|
+
// `moot` is deliberately not editable here — mooting an assumption is a
|
|
331
|
+
// gated business action (see core `relations.ts`), not a free-form toggle.
|
|
332
|
+
],
|
|
333
|
+
experiments: [
|
|
334
|
+
t("Title", "Experiment"),
|
|
335
|
+
t("Instrument", "Instrument"),
|
|
336
|
+
sel("Feasibility", "Feasibility", ["High", "Medium", "Low"], true),
|
|
337
|
+
sel("Status", "Status", ["Running", "Closed"]),
|
|
338
|
+
sel(
|
|
339
|
+
"closureReason",
|
|
340
|
+
"Closure reason",
|
|
341
|
+
["Completed", "Early-stop", "Kill"],
|
|
342
|
+
true
|
|
343
|
+
),
|
|
344
|
+
t("Date", "Date")
|
|
345
|
+
],
|
|
346
|
+
readings: [
|
|
347
|
+
t("Title", "Reading"),
|
|
348
|
+
t("Source", "Source"),
|
|
349
|
+
sel("Rung", "Rung", READING_RUNGS),
|
|
350
|
+
sel("Result", "Result", ["Validated", "Invalidated", "Inconclusive"]),
|
|
351
|
+
sel("Representativeness", "Representativeness", SOURCE_QUALITY),
|
|
352
|
+
sel("Credibility", "Credibility", SOURCE_QUALITY),
|
|
353
|
+
sel("magnitudeBand", "Magnitude band", ["Low", "Typical", "High"], true),
|
|
354
|
+
t("Date", "Date")
|
|
355
|
+
],
|
|
356
|
+
goals: [
|
|
357
|
+
t("Title", "Goal"),
|
|
358
|
+
sel("Status", "Status", ["Draft", "Active", "Closed"]),
|
|
359
|
+
sel("Outcome", "Outcome", ["Achieved", "Missed", "Dropped"], true),
|
|
360
|
+
t("Date", "Date")
|
|
361
|
+
],
|
|
362
|
+
decisions: [
|
|
363
|
+
t("Title", "Decision"),
|
|
364
|
+
sel("Status", "Status", ["Active", "Provisional", "Superseded", "Reversed"])
|
|
365
|
+
],
|
|
366
|
+
glossary: [
|
|
367
|
+
t("Title", "Term"),
|
|
368
|
+
sel("Status", "Status", ["Active", "Provisional", "Superseded"])
|
|
369
|
+
],
|
|
370
|
+
people: [t("Name", "Name"), t("Role", "Role"), t("Segment", "Segment")]
|
|
371
|
+
};
|
|
372
|
+
function editableFields(register) {
|
|
373
|
+
return EDITORS[register];
|
|
374
|
+
}
|
|
375
|
+
function draftFrom(register, record) {
|
|
376
|
+
const draft = {};
|
|
377
|
+
for (const f of editableFields(register)) {
|
|
378
|
+
const value = record[f.key];
|
|
379
|
+
draft[f.key] = value === null || value === void 0 ? "" : String(value);
|
|
380
|
+
}
|
|
381
|
+
return draft;
|
|
382
|
+
}
|
|
383
|
+
function coerce(field, raw) {
|
|
384
|
+
const str2 = String(raw);
|
|
385
|
+
if (field.kind === "number") {
|
|
386
|
+
if (str2.trim() === "") return null;
|
|
387
|
+
const n = Number(str2);
|
|
388
|
+
return Number.isNaN(n) ? null : n;
|
|
389
|
+
}
|
|
390
|
+
if (str2 === "" && (field.kind === "select" || field.nullable)) return null;
|
|
391
|
+
return str2;
|
|
392
|
+
}
|
|
393
|
+
function norm(value) {
|
|
394
|
+
return value === void 0 || value === "" ? null : value;
|
|
395
|
+
}
|
|
396
|
+
function buildPatch(register, original, draft) {
|
|
397
|
+
const patch = { version: original.version };
|
|
398
|
+
for (const field of editableFields(register)) {
|
|
399
|
+
const next = coerce(field, draft[field.key] ?? "");
|
|
400
|
+
if (norm(next) !== norm(original[field.key])) patch[field.key] = next;
|
|
401
|
+
}
|
|
402
|
+
return patch;
|
|
403
|
+
}
|
|
404
|
+
function hasEdits(register, original, draft) {
|
|
405
|
+
return Object.keys(buildPatch(register, original, draft)).length > 1;
|
|
406
|
+
}
|
|
331
407
|
|
|
332
408
|
// src/use-records.ts
|
|
333
409
|
import { useCallback, useEffect as useEffect2, useState } from "react";
|
|
@@ -362,7 +438,7 @@ function useJsonResource(url) {
|
|
|
362
438
|
live = false;
|
|
363
439
|
};
|
|
364
440
|
}, [url, tick]);
|
|
365
|
-
const refresh = useCallback(() => setTick((
|
|
441
|
+
const refresh = useCallback(() => setTick((t2) => t2 + 1), []);
|
|
366
442
|
return { ...state, refresh };
|
|
367
443
|
}
|
|
368
444
|
function useList(register, basePath = "/api") {
|
|
@@ -373,26 +449,1020 @@ function useList(register, basePath = "/api") {
|
|
|
373
449
|
}
|
|
374
450
|
function useRecord(register, id, basePath = "/api") {
|
|
375
451
|
const url = id ? `${basePath}/${register}/${encodeURIComponent(id)}` : null;
|
|
376
|
-
const { data, loading, error } = useJsonResource(url);
|
|
377
|
-
return { record: data, loading, error };
|
|
452
|
+
const { data, loading, error, refresh } = useJsonResource(url);
|
|
453
|
+
return { record: data, loading, error, refresh };
|
|
454
|
+
}
|
|
455
|
+
var SAVE_FAILED = "Couldn't save your changes \u2014 please try again.";
|
|
456
|
+
function interpretSave(status, body) {
|
|
457
|
+
if (status >= 200 && status < 300) {
|
|
458
|
+
return { ok: true, record: pickData(body) };
|
|
459
|
+
}
|
|
460
|
+
const message = body?.message;
|
|
461
|
+
const text = typeof message === "string" ? message : null;
|
|
462
|
+
if (status === 409) {
|
|
463
|
+
return { ok: false, conflict: true, message: text ?? CONFLICT_MESSAGE };
|
|
464
|
+
}
|
|
465
|
+
return { ok: false, conflict: false, message: text ?? SAVE_FAILED };
|
|
466
|
+
}
|
|
467
|
+
function useUpdate(register, basePath = "/api") {
|
|
468
|
+
const [saving, setSaving] = useState(false);
|
|
469
|
+
const [conflict, setConflict] = useState(null);
|
|
470
|
+
const [error, setError] = useState(null);
|
|
471
|
+
const save = useCallback(
|
|
472
|
+
async (id, patch) => {
|
|
473
|
+
setSaving(true);
|
|
474
|
+
setConflict(null);
|
|
475
|
+
setError(null);
|
|
476
|
+
try {
|
|
477
|
+
const res = await fetch(
|
|
478
|
+
`${basePath}/${register}/${encodeURIComponent(id)}`,
|
|
479
|
+
{
|
|
480
|
+
method: "PATCH",
|
|
481
|
+
headers: { "content-type": "application/json" },
|
|
482
|
+
body: JSON.stringify(patch)
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
const body = await res.json().catch(() => null);
|
|
486
|
+
const result = interpretSave(res.status, body);
|
|
487
|
+
if (!result.ok && result.conflict) setConflict(result.message);
|
|
488
|
+
else if (!result.ok) setError(result.message);
|
|
489
|
+
return result;
|
|
490
|
+
} catch (e) {
|
|
491
|
+
const message = e instanceof Error ? e.message : "Couldn't save your changes.";
|
|
492
|
+
setError(message);
|
|
493
|
+
return { ok: false, conflict: false, message };
|
|
494
|
+
} finally {
|
|
495
|
+
setSaving(false);
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
[register, basePath]
|
|
499
|
+
);
|
|
500
|
+
const reset = useCallback(() => {
|
|
501
|
+
setConflict(null);
|
|
502
|
+
setError(null);
|
|
503
|
+
}, []);
|
|
504
|
+
return { save, saving, conflict, error, reset };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/understanding.ts
|
|
508
|
+
import {
|
|
509
|
+
toReadingInput
|
|
510
|
+
} from "@validation-os/core";
|
|
511
|
+
import {
|
|
512
|
+
confidenceAttribution,
|
|
513
|
+
confidenceTrajectory,
|
|
514
|
+
experimentProgress,
|
|
515
|
+
isConcluded
|
|
516
|
+
} from "@validation-os/core/derivation";
|
|
517
|
+
function str(v) {
|
|
518
|
+
return typeof v === "string" && v !== "" ? v : null;
|
|
519
|
+
}
|
|
520
|
+
function testsAssumption(exp, assumptionId) {
|
|
521
|
+
const bars = exp.barLines;
|
|
522
|
+
if (bars?.some((b) => b.assumptionId === assumptionId)) return true;
|
|
523
|
+
const ids = exp.barLineAssumptionIds;
|
|
524
|
+
return ids?.includes(assumptionId) ?? false;
|
|
525
|
+
}
|
|
526
|
+
function buildUnderstanding(assumption, readings, experiments) {
|
|
527
|
+
const mine = readings.filter((r) => r.assumptionId === assumption.id);
|
|
528
|
+
const inputs = mine.map(toReadingInput);
|
|
529
|
+
const { confidence, movers } = confidenceAttribution(inputs);
|
|
530
|
+
const experimentsById = new Map(experiments.map((e) => [e.id, e]));
|
|
531
|
+
const moverByExperiment = new Map(
|
|
532
|
+
movers.filter((m) => m.kind === "experiment").map((m) => [m.experimentId, m])
|
|
533
|
+
);
|
|
534
|
+
const experimentIds = /* @__PURE__ */ new Set([
|
|
535
|
+
...experiments.filter((e) => testsAssumption(e, assumption.id)).map((e) => e.id),
|
|
536
|
+
...moverByExperiment.keys()
|
|
537
|
+
]);
|
|
538
|
+
const experimentViews = [...experimentIds].map((id) => {
|
|
539
|
+
const exp = experimentsById.get(id);
|
|
540
|
+
const mover = moverByExperiment.get(id);
|
|
541
|
+
const bars = exp?.barLines ?? [];
|
|
542
|
+
const progress = bars.length ? experimentProgress(bars) : null;
|
|
543
|
+
const status = exp ? str(exp.Status) : null;
|
|
544
|
+
return {
|
|
545
|
+
experimentId: id,
|
|
546
|
+
title: exp ? str(exp.Title) : null,
|
|
547
|
+
status,
|
|
548
|
+
contribution: mover?.contribution ?? 0,
|
|
549
|
+
magnitude: mover?.magnitude ?? 0,
|
|
550
|
+
readingCount: mover?.readingCount ?? 0,
|
|
551
|
+
progress,
|
|
552
|
+
done: status === "Closed" || progress?.concluded === true
|
|
553
|
+
};
|
|
554
|
+
});
|
|
555
|
+
experimentViews.sort(
|
|
556
|
+
(a, b) => b.magnitude - a.magnitude || Number(a.done) - Number(b.done) || a.experimentId.localeCompare(b.experimentId)
|
|
557
|
+
);
|
|
558
|
+
const otherMovers = movers.filter((m) => m.kind !== "experiment").map((m) => ({
|
|
559
|
+
key: m.key,
|
|
560
|
+
kind: m.kind,
|
|
561
|
+
goalId: m.goalId,
|
|
562
|
+
contribution: m.contribution,
|
|
563
|
+
magnitude: m.magnitude,
|
|
564
|
+
readingCount: m.readingCount
|
|
565
|
+
}));
|
|
566
|
+
return {
|
|
567
|
+
confidence,
|
|
568
|
+
experiments: experimentViews,
|
|
569
|
+
otherMovers,
|
|
570
|
+
trajectory: confidenceTrajectory(inputs),
|
|
571
|
+
readingCount: inputs.filter((r) => isConcluded(r.result)).length
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/understanding-panel.tsx
|
|
576
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
577
|
+
function UnderstandingPanel({
|
|
578
|
+
assumption,
|
|
579
|
+
basePath
|
|
580
|
+
}) {
|
|
581
|
+
const readings = useList("readings", basePath);
|
|
582
|
+
const experiments = useList("experiments", basePath);
|
|
583
|
+
if (readings.loading || experiments.loading) {
|
|
584
|
+
return /* @__PURE__ */ jsx4(Muted, { children: "Working out what moved the number\u2026" });
|
|
585
|
+
}
|
|
586
|
+
if (readings.error || experiments.error) {
|
|
587
|
+
return /* @__PURE__ */ jsx4(Muted, { children: "Couldn't load the evidence behind this number." });
|
|
588
|
+
}
|
|
589
|
+
const u = buildUnderstanding(
|
|
590
|
+
assumption,
|
|
591
|
+
readings.records ?? [],
|
|
592
|
+
experiments.records ?? []
|
|
593
|
+
);
|
|
594
|
+
if (u.experiments.length === 0 && u.otherMovers.length === 0) {
|
|
595
|
+
return /* @__PURE__ */ jsx4(Muted, { children: "No experiments or concluded readings yet \u2014 Confidence rests on the neutral prior alone." });
|
|
596
|
+
}
|
|
597
|
+
const maxMagnitude = Math.max(
|
|
598
|
+
...u.experiments.map((e) => e.magnitude),
|
|
599
|
+
...u.otherMovers.map((m) => m.magnitude),
|
|
600
|
+
0.01
|
|
601
|
+
);
|
|
602
|
+
return /* @__PURE__ */ jsxs4("div", { className: "mt-2 space-y-4 rounded-lg bg-neutral-50 p-3 dark:bg-neutral-900", children: [
|
|
603
|
+
u.experiments.length ? /* @__PURE__ */ jsxs4("section", { children: [
|
|
604
|
+
/* @__PURE__ */ jsx4(Heading, { children: "What's moving Confidence" }),
|
|
605
|
+
/* @__PURE__ */ jsx4("ul", { className: "space-y-2.5", children: u.experiments.map((e) => /* @__PURE__ */ jsxs4("li", { children: [
|
|
606
|
+
/* @__PURE__ */ jsx4(
|
|
607
|
+
PushRow,
|
|
608
|
+
{
|
|
609
|
+
label: e.title ?? `Experiment ${e.experimentId}`,
|
|
610
|
+
contribution: e.contribution,
|
|
611
|
+
magnitude: e.magnitude,
|
|
612
|
+
max: maxMagnitude
|
|
613
|
+
}
|
|
614
|
+
),
|
|
615
|
+
/* @__PURE__ */ jsx4("p", { className: "mt-1 text-xs text-neutral-500 dark:text-neutral-400", children: experimentDetail(e) })
|
|
616
|
+
] }, e.experimentId)) })
|
|
617
|
+
] }) : null,
|
|
618
|
+
u.otherMovers.length ? /* @__PURE__ */ jsxs4("section", { children: [
|
|
619
|
+
/* @__PURE__ */ jsx4(Heading, { children: "Other evidence" }),
|
|
620
|
+
/* @__PURE__ */ jsx4("ul", { className: "space-y-2.5", children: u.otherMovers.map((m) => /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsx4(
|
|
621
|
+
PushRow,
|
|
622
|
+
{
|
|
623
|
+
label: otherMoverLabel(m),
|
|
624
|
+
contribution: m.contribution,
|
|
625
|
+
magnitude: m.magnitude,
|
|
626
|
+
max: maxMagnitude
|
|
627
|
+
}
|
|
628
|
+
) }, m.key)) })
|
|
629
|
+
] }) : null,
|
|
630
|
+
/* @__PURE__ */ jsxs4("section", { children: [
|
|
631
|
+
/* @__PURE__ */ jsx4(Heading, { children: "Confidence over time" }),
|
|
632
|
+
/* @__PURE__ */ jsx4(Trajectory, { points: u.trajectory })
|
|
633
|
+
] })
|
|
634
|
+
] });
|
|
635
|
+
}
|
|
636
|
+
function PushRow({
|
|
637
|
+
label,
|
|
638
|
+
contribution,
|
|
639
|
+
magnitude,
|
|
640
|
+
max
|
|
641
|
+
}) {
|
|
642
|
+
const up = contribution >= 0;
|
|
643
|
+
const moving = magnitude > 0;
|
|
644
|
+
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
645
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-baseline justify-between gap-2", children: [
|
|
646
|
+
/* @__PURE__ */ jsx4("span", { className: "truncate text-sm text-neutral-800 dark:text-neutral-200", children: label }),
|
|
647
|
+
moving ? /* @__PURE__ */ jsxs4(
|
|
648
|
+
"span",
|
|
649
|
+
{
|
|
650
|
+
className: `shrink-0 text-sm font-semibold tabular-nums ${up ? "text-emerald-600 dark:text-emerald-400" : "text-rose-600 dark:text-rose-400"}`,
|
|
651
|
+
children: [
|
|
652
|
+
up ? "+" : "",
|
|
653
|
+
contribution
|
|
654
|
+
]
|
|
655
|
+
}
|
|
656
|
+
) : /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-xs text-neutral-400", children: "no readings yet" })
|
|
657
|
+
] }),
|
|
658
|
+
/* @__PURE__ */ jsx4("div", { className: "mt-1 h-1.5 rounded-full bg-neutral-200 dark:bg-neutral-800", children: /* @__PURE__ */ jsx4(
|
|
659
|
+
"div",
|
|
660
|
+
{
|
|
661
|
+
className: `h-full rounded-full ${up ? "bg-emerald-500" : "bg-rose-500"}`,
|
|
662
|
+
style: { width: `${magnitude / max * 100}%` }
|
|
663
|
+
}
|
|
664
|
+
) })
|
|
665
|
+
] });
|
|
666
|
+
}
|
|
667
|
+
function Trajectory({ points }) {
|
|
668
|
+
if (points.length < 2) {
|
|
669
|
+
return /* @__PURE__ */ jsx4("p", { className: "text-xs text-neutral-500 dark:text-neutral-400", children: points.length === 1 ? `One dated reading so far \u2014 Confidence ${points[0].confidence} on ${points[0].date}.` : "No dated readings yet to chart a trajectory." });
|
|
670
|
+
}
|
|
671
|
+
return /* @__PURE__ */ jsx4(Sparkline, { points });
|
|
672
|
+
}
|
|
673
|
+
var SPARK_W = 260;
|
|
674
|
+
var SPARK_H = 44;
|
|
675
|
+
function Sparkline({ points }) {
|
|
676
|
+
const n = points.length;
|
|
677
|
+
const x = (i) => i / (n - 1) * SPARK_W;
|
|
678
|
+
const y = (c) => (100 - c) / 200 * SPARK_H;
|
|
679
|
+
const path = points.map((p, i) => `${x(i)},${y(p.confidence)}`).join(" ");
|
|
680
|
+
const first = points[0];
|
|
681
|
+
const last = points[n - 1];
|
|
682
|
+
return /* @__PURE__ */ jsxs4("div", { children: [
|
|
683
|
+
/* @__PURE__ */ jsxs4(
|
|
684
|
+
"svg",
|
|
685
|
+
{
|
|
686
|
+
viewBox: `0 0 ${SPARK_W} ${SPARK_H}`,
|
|
687
|
+
className: "w-full",
|
|
688
|
+
role: "img",
|
|
689
|
+
"aria-label": `Confidence moved from ${first.confidence} to ${last.confidence}`,
|
|
690
|
+
children: [
|
|
691
|
+
/* @__PURE__ */ jsx4(
|
|
692
|
+
"line",
|
|
693
|
+
{
|
|
694
|
+
x1: 0,
|
|
695
|
+
x2: SPARK_W,
|
|
696
|
+
y1: y(0),
|
|
697
|
+
y2: y(0),
|
|
698
|
+
className: "stroke-neutral-300 dark:stroke-neutral-700",
|
|
699
|
+
strokeWidth: 1,
|
|
700
|
+
strokeDasharray: "3 3"
|
|
701
|
+
}
|
|
702
|
+
),
|
|
703
|
+
/* @__PURE__ */ jsx4(
|
|
704
|
+
"polyline",
|
|
705
|
+
{
|
|
706
|
+
points: path,
|
|
707
|
+
fill: "none",
|
|
708
|
+
className: "stroke-neutral-700 dark:stroke-neutral-200",
|
|
709
|
+
strokeWidth: 1.5,
|
|
710
|
+
strokeLinejoin: "round",
|
|
711
|
+
strokeLinecap: "round"
|
|
712
|
+
}
|
|
713
|
+
),
|
|
714
|
+
/* @__PURE__ */ jsx4(
|
|
715
|
+
"circle",
|
|
716
|
+
{
|
|
717
|
+
cx: x(n - 1),
|
|
718
|
+
cy: y(last.confidence),
|
|
719
|
+
r: 2.5,
|
|
720
|
+
className: "fill-neutral-900 dark:fill-neutral-50"
|
|
721
|
+
}
|
|
722
|
+
)
|
|
723
|
+
]
|
|
724
|
+
}
|
|
725
|
+
),
|
|
726
|
+
/* @__PURE__ */ jsxs4("div", { className: "mt-1 flex justify-between text-[11px] text-neutral-400", children: [
|
|
727
|
+
/* @__PURE__ */ jsx4("span", { children: first.date }),
|
|
728
|
+
/* @__PURE__ */ jsxs4("span", { className: "tabular-nums text-neutral-600 dark:text-neutral-300", children: [
|
|
729
|
+
"now ",
|
|
730
|
+
last.confidence
|
|
731
|
+
] })
|
|
732
|
+
] })
|
|
733
|
+
] });
|
|
734
|
+
}
|
|
735
|
+
function experimentDetail(e) {
|
|
736
|
+
const evidence = e.readingCount === 0 ? "No readings yet" : `${e.readingCount} reading${e.readingCount === 1 ? "" : "s"}`;
|
|
737
|
+
const p = e.progress;
|
|
738
|
+
if (e.done) {
|
|
739
|
+
return p ? `Concluded \xB7 ${p.total} of ${p.total} bars settled` : `Concluded \xB7 ${evidence}`;
|
|
740
|
+
}
|
|
741
|
+
if (!p || p.total === 0) return `${evidence} \xB7 no pre-registered bars`;
|
|
742
|
+
return `${evidence} \xB7 ${p.settled} of ${p.total} bars settled \xB7 ${p.toGo} to go`;
|
|
743
|
+
}
|
|
744
|
+
function otherMoverLabel(m) {
|
|
745
|
+
if (m.kind === "goal") return `Goal ${m.goalId ?? ""}`.trim();
|
|
746
|
+
return m.readingCount === 1 ? "A direct reading" : "Direct readings";
|
|
747
|
+
}
|
|
748
|
+
function Heading({ children }) {
|
|
749
|
+
return /* @__PURE__ */ jsx4("h4", { className: "mb-2 text-xs font-medium uppercase tracking-wide text-neutral-400", children });
|
|
750
|
+
}
|
|
751
|
+
function Muted({ children }) {
|
|
752
|
+
return /* @__PURE__ */ jsx4("p", { className: "mt-2 rounded-lg bg-neutral-50 p-3 text-xs leading-relaxed text-neutral-600 dark:bg-neutral-900 dark:text-neutral-300", children });
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/record-drawer.tsx
|
|
756
|
+
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
757
|
+
var META_FIELDS = /* @__PURE__ */ new Set(["id", "version", "createdAt", "updatedAt", "derived"]);
|
|
758
|
+
function RecordDrawer({
|
|
759
|
+
register,
|
|
760
|
+
record,
|
|
761
|
+
loading,
|
|
762
|
+
error,
|
|
763
|
+
open,
|
|
764
|
+
onClose,
|
|
765
|
+
basePath,
|
|
766
|
+
onChanged,
|
|
767
|
+
children
|
|
768
|
+
}) {
|
|
769
|
+
const [editing, setEditing] = useState2(false);
|
|
770
|
+
const [draft, setDraft] = useState2({});
|
|
771
|
+
const [baseline, setBaseline] = useState2(null);
|
|
772
|
+
const [why, setWhy] = useState2(false);
|
|
773
|
+
const { save, saving, conflict, error: saveError, reset } = useUpdate(
|
|
774
|
+
register,
|
|
775
|
+
basePath
|
|
776
|
+
);
|
|
777
|
+
const recordId = record?.id ?? null;
|
|
778
|
+
useEffect3(() => {
|
|
779
|
+
setEditing(false);
|
|
780
|
+
setBaseline(null);
|
|
781
|
+
setWhy(false);
|
|
782
|
+
reset();
|
|
783
|
+
}, [recordId, reset]);
|
|
784
|
+
const derived = record && record.derived && typeof record.derived === "object" ? record.derived : null;
|
|
785
|
+
const fields = record ? Object.keys(record).filter((k) => !META_FIELDS.has(k)) : [];
|
|
786
|
+
function startEditing() {
|
|
787
|
+
if (!record) return;
|
|
788
|
+
setBaseline(record);
|
|
789
|
+
setDraft(draftFrom(register, record));
|
|
790
|
+
reset();
|
|
791
|
+
setEditing(true);
|
|
792
|
+
}
|
|
793
|
+
function cancelEditing() {
|
|
794
|
+
setEditing(false);
|
|
795
|
+
reset();
|
|
796
|
+
}
|
|
797
|
+
async function onSave() {
|
|
798
|
+
if (!record || !baseline) return;
|
|
799
|
+
const patch = buildPatch(register, baseline, draft);
|
|
800
|
+
patch.version = record.version;
|
|
801
|
+
if (Object.keys(patch).length <= 1) {
|
|
802
|
+
setEditing(false);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const result = await save(record.id, patch);
|
|
806
|
+
if (result.ok) {
|
|
807
|
+
setEditing(false);
|
|
808
|
+
onChanged?.();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function reloadLatest() {
|
|
812
|
+
reset();
|
|
813
|
+
onChanged?.();
|
|
814
|
+
}
|
|
815
|
+
const setField = (key, value) => setDraft((d) => ({ ...d, [key]: value }));
|
|
816
|
+
return /* @__PURE__ */ jsxs5(
|
|
817
|
+
DrawerShell,
|
|
818
|
+
{
|
|
819
|
+
open,
|
|
820
|
+
onClose,
|
|
821
|
+
ariaLabel: `${REGISTER_LABEL[register]} record`,
|
|
822
|
+
children: [
|
|
823
|
+
/* @__PURE__ */ jsxs5("header", { className: "flex items-start justify-between gap-4 border-b border-neutral-200 p-5 dark:border-neutral-800", children: [
|
|
824
|
+
/* @__PURE__ */ jsxs5("div", { children: [
|
|
825
|
+
/* @__PURE__ */ jsx5("p", { className: "text-xs font-medium uppercase tracking-wide text-neutral-400", children: REGISTER_LABEL[register] }),
|
|
826
|
+
/* @__PURE__ */ jsx5("h2", { className: "mt-1 text-lg font-semibold text-neutral-900 dark:text-neutral-50", children: record ? primaryLabel(record) : loading ? "Loading\u2026" : "\u2014" })
|
|
827
|
+
] }),
|
|
828
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [
|
|
829
|
+
record && !editing ? /* @__PURE__ */ jsx5(
|
|
830
|
+
"button",
|
|
831
|
+
{
|
|
832
|
+
type: "button",
|
|
833
|
+
onClick: startEditing,
|
|
834
|
+
className: "rounded-md border border-neutral-300 px-2.5 py-1 text-sm font-medium text-neutral-700 hover:bg-neutral-100 dark:border-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-900",
|
|
835
|
+
children: "Edit"
|
|
836
|
+
}
|
|
837
|
+
) : null,
|
|
838
|
+
/* @__PURE__ */ jsx5(
|
|
839
|
+
"button",
|
|
840
|
+
{
|
|
841
|
+
type: "button",
|
|
842
|
+
onClick: onClose,
|
|
843
|
+
className: "rounded-md px-2 py-1 text-sm text-neutral-500 hover:bg-neutral-100 dark:hover:bg-neutral-900",
|
|
844
|
+
children: "Close"
|
|
845
|
+
}
|
|
846
|
+
)
|
|
847
|
+
] })
|
|
848
|
+
] }),
|
|
849
|
+
/* @__PURE__ */ jsx5("div", { className: "flex-1 p-5", children: loading ? /* @__PURE__ */ jsx5("p", { className: "text-sm text-neutral-500", children: "Loading record\u2026" }) : error ? /* @__PURE__ */ jsx5("p", { className: "text-sm text-red-600 dark:text-red-400", children: error }) : !record ? /* @__PURE__ */ jsx5("p", { className: "text-sm text-neutral-500", children: "No record." }) : /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
850
|
+
derived ? /* @__PURE__ */ jsxs5("section", { className: "mb-6", children: [
|
|
851
|
+
/* @__PURE__ */ jsx5("h3", { className: "mb-2 text-xs font-medium uppercase tracking-wide text-neutral-400", children: "Computed \xB7 not editable" }),
|
|
852
|
+
/* @__PURE__ */ jsx5("dl", { className: "grid grid-cols-3 gap-3", children: Object.entries(derived).map(([key, value]) => /* @__PURE__ */ jsxs5(
|
|
853
|
+
"div",
|
|
854
|
+
{
|
|
855
|
+
className: "rounded-lg border border-neutral-200 bg-neutral-50 p-3 dark:border-neutral-800 dark:bg-neutral-900",
|
|
856
|
+
children: [
|
|
857
|
+
/* @__PURE__ */ jsx5("dt", { className: "text-xs text-neutral-500 dark:text-neutral-400", children: derivedLabel(key) }),
|
|
858
|
+
/* @__PURE__ */ jsx5("dd", { className: "mt-1 text-xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-50", children: formatValue(value) })
|
|
859
|
+
]
|
|
860
|
+
},
|
|
861
|
+
key
|
|
862
|
+
)) }),
|
|
863
|
+
"confidence" in derived ? /* @__PURE__ */ jsx5(
|
|
864
|
+
WhyReveal,
|
|
865
|
+
{
|
|
866
|
+
open: why,
|
|
867
|
+
onToggle: () => setWhy((w) => !w),
|
|
868
|
+
assumption: record,
|
|
869
|
+
basePath
|
|
870
|
+
}
|
|
871
|
+
) : null
|
|
872
|
+
] }) : null,
|
|
873
|
+
conflict ? /* @__PURE__ */ jsx5(ConflictBanner, { message: conflict, onReload: reloadLatest }) : null,
|
|
874
|
+
saveError ? /* @__PURE__ */ jsx5(
|
|
875
|
+
"p",
|
|
876
|
+
{
|
|
877
|
+
role: "alert",
|
|
878
|
+
className: "mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300",
|
|
879
|
+
children: saveError
|
|
880
|
+
}
|
|
881
|
+
) : null,
|
|
882
|
+
editing ? /* @__PURE__ */ jsx5(
|
|
883
|
+
EditFields,
|
|
884
|
+
{
|
|
885
|
+
register,
|
|
886
|
+
draft,
|
|
887
|
+
onField: setField
|
|
888
|
+
}
|
|
889
|
+
) : /* @__PURE__ */ jsx5("dl", { className: "divide-y divide-neutral-100 dark:divide-neutral-900", children: fields.map((key) => /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-3 gap-3 py-2.5", children: [
|
|
890
|
+
/* @__PURE__ */ jsx5("dt", { className: "text-sm text-neutral-500 dark:text-neutral-400", children: fieldLabel(key) }),
|
|
891
|
+
/* @__PURE__ */ jsx5("dd", { className: "col-span-2 text-sm text-neutral-800 dark:text-neutral-200", children: formatValue(record[key]) })
|
|
892
|
+
] }, key)) })
|
|
893
|
+
] }) }),
|
|
894
|
+
record && !loading && !error && !editing ? children : null,
|
|
895
|
+
record && editing ? /* @__PURE__ */ jsxs5("footer", { className: "flex items-center justify-end gap-2 border-t border-neutral-200 p-5 dark:border-neutral-800", children: [
|
|
896
|
+
/* @__PURE__ */ jsx5(
|
|
897
|
+
"button",
|
|
898
|
+
{
|
|
899
|
+
type: "button",
|
|
900
|
+
onClick: cancelEditing,
|
|
901
|
+
disabled: saving,
|
|
902
|
+
className: "rounded-md px-3 py-1.5 text-sm text-neutral-600 hover:bg-neutral-100 disabled:opacity-50 dark:text-neutral-300 dark:hover:bg-neutral-900",
|
|
903
|
+
children: "Cancel"
|
|
904
|
+
}
|
|
905
|
+
),
|
|
906
|
+
/* @__PURE__ */ jsx5(
|
|
907
|
+
"button",
|
|
908
|
+
{
|
|
909
|
+
type: "button",
|
|
910
|
+
onClick: onSave,
|
|
911
|
+
disabled: saving,
|
|
912
|
+
className: "rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white hover:bg-neutral-700 disabled:opacity-50 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-300",
|
|
913
|
+
children: saving ? "Saving\u2026" : "Save"
|
|
914
|
+
}
|
|
915
|
+
)
|
|
916
|
+
] }) : record ? /* @__PURE__ */ jsxs5("footer", { className: "border-t border-neutral-200 p-5 text-xs text-neutral-400 dark:border-neutral-800", children: [
|
|
917
|
+
record.id,
|
|
918
|
+
" \xB7 updated ",
|
|
919
|
+
formatValue(record.updatedAt)
|
|
920
|
+
] }) : null
|
|
921
|
+
]
|
|
922
|
+
}
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
function WhyReveal({
|
|
926
|
+
open,
|
|
927
|
+
onToggle,
|
|
928
|
+
assumption,
|
|
929
|
+
basePath
|
|
930
|
+
}) {
|
|
931
|
+
return /* @__PURE__ */ jsxs5("div", { className: "mt-3", children: [
|
|
932
|
+
/* @__PURE__ */ jsx5(
|
|
933
|
+
"button",
|
|
934
|
+
{
|
|
935
|
+
type: "button",
|
|
936
|
+
onClick: onToggle,
|
|
937
|
+
"aria-expanded": open,
|
|
938
|
+
className: "text-xs font-medium text-neutral-500 underline underline-offset-2 hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-100",
|
|
939
|
+
children: "Why?"
|
|
940
|
+
}
|
|
941
|
+
),
|
|
942
|
+
open ? /* @__PURE__ */ jsx5(UnderstandingPanel, { assumption, basePath }) : null
|
|
943
|
+
] });
|
|
944
|
+
}
|
|
945
|
+
function ConflictBanner({
|
|
946
|
+
message,
|
|
947
|
+
onReload
|
|
948
|
+
}) {
|
|
949
|
+
return /* @__PURE__ */ jsxs5(
|
|
950
|
+
"div",
|
|
951
|
+
{
|
|
952
|
+
role: "alert",
|
|
953
|
+
className: "mb-4 rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900 dark:bg-amber-950/40",
|
|
954
|
+
children: [
|
|
955
|
+
/* @__PURE__ */ jsx5("p", { className: "text-sm text-amber-800 dark:text-amber-200", children: message }),
|
|
956
|
+
/* @__PURE__ */ jsx5(
|
|
957
|
+
"button",
|
|
958
|
+
{
|
|
959
|
+
type: "button",
|
|
960
|
+
onClick: onReload,
|
|
961
|
+
className: "mt-2 rounded-md border border-amber-300 px-2.5 py-1 text-xs font-medium text-amber-900 hover:bg-amber-100 dark:border-amber-800 dark:text-amber-100 dark:hover:bg-amber-900/40",
|
|
962
|
+
children: "Reload the latest"
|
|
963
|
+
}
|
|
964
|
+
)
|
|
965
|
+
]
|
|
966
|
+
}
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
function EditFields({
|
|
970
|
+
register,
|
|
971
|
+
draft,
|
|
972
|
+
onField
|
|
973
|
+
}) {
|
|
974
|
+
return /* @__PURE__ */ jsx5("div", { className: "space-y-4", children: editableFields(register).map((field) => /* @__PURE__ */ jsx5(
|
|
975
|
+
FieldInput,
|
|
976
|
+
{
|
|
977
|
+
field,
|
|
978
|
+
value: draft[field.key],
|
|
979
|
+
onChange: (v) => onField(field.key, v)
|
|
980
|
+
},
|
|
981
|
+
field.key
|
|
982
|
+
)) });
|
|
983
|
+
}
|
|
984
|
+
var INPUT_CLASS = "w-full rounded-md border border-neutral-300 bg-white px-2.5 py-1.5 text-sm text-neutral-900 focus:border-neutral-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-50";
|
|
985
|
+
function FieldInput({
|
|
986
|
+
field,
|
|
987
|
+
value,
|
|
988
|
+
onChange
|
|
989
|
+
}) {
|
|
990
|
+
const id = `field-${field.key.replace(/\s+/g, "-")}`;
|
|
991
|
+
return /* @__PURE__ */ jsxs5("div", { children: [
|
|
992
|
+
/* @__PURE__ */ jsx5(
|
|
993
|
+
"label",
|
|
994
|
+
{
|
|
995
|
+
htmlFor: id,
|
|
996
|
+
className: "mb-1 block text-xs font-medium text-neutral-500 dark:text-neutral-400",
|
|
997
|
+
children: field.label
|
|
998
|
+
}
|
|
999
|
+
),
|
|
1000
|
+
field.kind === "textarea" ? /* @__PURE__ */ jsx5(
|
|
1001
|
+
"textarea",
|
|
1002
|
+
{
|
|
1003
|
+
id,
|
|
1004
|
+
value: String(value ?? ""),
|
|
1005
|
+
onChange: (e) => onChange(e.target.value),
|
|
1006
|
+
rows: 3,
|
|
1007
|
+
className: INPUT_CLASS
|
|
1008
|
+
}
|
|
1009
|
+
) : field.kind === "select" ? /* @__PURE__ */ jsxs5(
|
|
1010
|
+
"select",
|
|
1011
|
+
{
|
|
1012
|
+
id,
|
|
1013
|
+
value: String(value ?? ""),
|
|
1014
|
+
onChange: (e) => onChange(e.target.value),
|
|
1015
|
+
className: INPUT_CLASS,
|
|
1016
|
+
children: [
|
|
1017
|
+
field.nullable ? /* @__PURE__ */ jsx5("option", { value: "", children: "\u2014" }) : null,
|
|
1018
|
+
field.options?.map((opt) => /* @__PURE__ */ jsx5("option", { value: opt, children: opt }, opt))
|
|
1019
|
+
]
|
|
1020
|
+
}
|
|
1021
|
+
) : /* @__PURE__ */ jsx5(
|
|
1022
|
+
"input",
|
|
1023
|
+
{
|
|
1024
|
+
id,
|
|
1025
|
+
type: field.kind === "number" ? "number" : "text",
|
|
1026
|
+
value: String(value ?? ""),
|
|
1027
|
+
onChange: (e) => onChange(e.target.value),
|
|
1028
|
+
className: INPUT_CLASS
|
|
1029
|
+
}
|
|
1030
|
+
)
|
|
1031
|
+
] });
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/record-form.tsx
|
|
1035
|
+
import { useMemo, useState as useState4 } from "react";
|
|
1036
|
+
|
|
1037
|
+
// src/form-fields.ts
|
|
1038
|
+
import {
|
|
1039
|
+
GOAL_RUNG_VALUES,
|
|
1040
|
+
TESTING_RUNGS
|
|
1041
|
+
} from "@validation-os/core";
|
|
1042
|
+
var ASSUMPTION_STATUS = ["Draft", "Live", "Invalidated"];
|
|
1043
|
+
var EXPERIMENT_STATUS = ["Running", "Closed"];
|
|
1044
|
+
var CLOSURE_REASON = ["Completed", "Early-stop", "Kill"];
|
|
1045
|
+
var GOAL_STATUS = ["Draft", "Active", "Closed"];
|
|
1046
|
+
var GOAL_OUTCOME = ["Achieved", "Missed", "Dropped"];
|
|
1047
|
+
var DECISION_STATUS = ["Active", "Provisional", "Superseded", "Reversed"];
|
|
1048
|
+
var GLOSSARY_STATUS = ["Active", "Provisional", "Superseded"];
|
|
1049
|
+
var RESULT = ["Validated", "Invalidated", "Inconclusive"];
|
|
1050
|
+
var MAGNITUDE = ["Low", "Typical", "High"];
|
|
1051
|
+
var FEASIBILITY = ["High", "Medium", "Low"];
|
|
1052
|
+
var RUNGS = [...TESTING_RUNGS, ...GOAL_RUNG_VALUES];
|
|
1053
|
+
var QUALITY = ["1", "0.7", "0.5"];
|
|
1054
|
+
var FIELDS = {
|
|
1055
|
+
assumptions: [
|
|
1056
|
+
{ key: "Title", label: "Assumption", kind: "text", required: true },
|
|
1057
|
+
{ key: "Description", label: "Description", kind: "textarea" },
|
|
1058
|
+
{ key: "Lens", label: "Lens", kind: "text" },
|
|
1059
|
+
{ key: "Impact", label: "Impact (0\u2013100)", kind: "number" },
|
|
1060
|
+
{ key: "Status", label: "Status", kind: "select", options: ASSUMPTION_STATUS },
|
|
1061
|
+
{ key: "5 Whys", label: "5 Whys", kind: "textarea" },
|
|
1062
|
+
{ key: "Metric for truth", label: "Metric for truth", kind: "textarea" },
|
|
1063
|
+
{
|
|
1064
|
+
key: "Scoring justification",
|
|
1065
|
+
label: "Scoring justification",
|
|
1066
|
+
kind: "textarea"
|
|
1067
|
+
}
|
|
1068
|
+
],
|
|
1069
|
+
experiments: [
|
|
1070
|
+
{ key: "Title", label: "Experiment", kind: "text", required: true },
|
|
1071
|
+
{ key: "Instrument", label: "Instrument", kind: "text" },
|
|
1072
|
+
{ key: "Feasibility", label: "Feasibility", kind: "select", options: FEASIBILITY },
|
|
1073
|
+
{ key: "Status", label: "Status", kind: "select", options: EXPERIMENT_STATUS },
|
|
1074
|
+
{
|
|
1075
|
+
key: "closureReason",
|
|
1076
|
+
label: "Closure reason",
|
|
1077
|
+
kind: "select",
|
|
1078
|
+
options: CLOSURE_REASON
|
|
1079
|
+
},
|
|
1080
|
+
{ key: "Date", label: "Date", kind: "text", placeholder: "YYYY-MM-DD" }
|
|
1081
|
+
],
|
|
1082
|
+
readings: [
|
|
1083
|
+
{ key: "Title", label: "Reading", kind: "text", required: true },
|
|
1084
|
+
{ key: "Source", label: "Source", kind: "text" },
|
|
1085
|
+
{ key: "Rung", label: "Rung", kind: "select", options: RUNGS, required: true },
|
|
1086
|
+
{ key: "Result", label: "Result", kind: "select", options: RESULT, required: true },
|
|
1087
|
+
{
|
|
1088
|
+
key: "Representativeness",
|
|
1089
|
+
label: "Representativeness",
|
|
1090
|
+
kind: "select",
|
|
1091
|
+
options: QUALITY,
|
|
1092
|
+
coerce: "number"
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
key: "Credibility",
|
|
1096
|
+
label: "Credibility",
|
|
1097
|
+
kind: "select",
|
|
1098
|
+
options: QUALITY,
|
|
1099
|
+
coerce: "number"
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
key: "magnitudeBand",
|
|
1103
|
+
label: "Magnitude band",
|
|
1104
|
+
kind: "select",
|
|
1105
|
+
options: MAGNITUDE
|
|
1106
|
+
},
|
|
1107
|
+
{ key: "Date", label: "Date", kind: "text", placeholder: "YYYY-MM-DD" }
|
|
1108
|
+
],
|
|
1109
|
+
goals: [
|
|
1110
|
+
{ key: "Title", label: "Goal", kind: "text", required: true },
|
|
1111
|
+
{ key: "Status", label: "Status", kind: "select", options: GOAL_STATUS },
|
|
1112
|
+
{ key: "Outcome", label: "Outcome", kind: "select", options: GOAL_OUTCOME },
|
|
1113
|
+
{ key: "Date", label: "Date", kind: "text", placeholder: "YYYY-MM-DD" }
|
|
1114
|
+
],
|
|
1115
|
+
decisions: [
|
|
1116
|
+
{ key: "Title", label: "Decision", kind: "text", required: true },
|
|
1117
|
+
{ key: "Status", label: "Status", kind: "select", options: DECISION_STATUS }
|
|
1118
|
+
],
|
|
1119
|
+
glossary: [
|
|
1120
|
+
{ key: "Title", label: "Term", kind: "text", required: true },
|
|
1121
|
+
{ key: "Status", label: "Status", kind: "select", options: GLOSSARY_STATUS }
|
|
1122
|
+
],
|
|
1123
|
+
people: [
|
|
1124
|
+
{ key: "Name", label: "Name", kind: "text", required: true },
|
|
1125
|
+
{ key: "Role", label: "Role", kind: "text" },
|
|
1126
|
+
{ key: "Segment", label: "Segment", kind: "text" }
|
|
1127
|
+
]
|
|
1128
|
+
};
|
|
1129
|
+
function formFieldsFor(register) {
|
|
1130
|
+
return FIELDS[register];
|
|
1131
|
+
}
|
|
1132
|
+
function emptyDraft(register) {
|
|
1133
|
+
const draft = {};
|
|
1134
|
+
for (const f of formFieldsFor(register)) draft[f.key] = "";
|
|
1135
|
+
return draft;
|
|
1136
|
+
}
|
|
1137
|
+
function missingRequired(register, draft) {
|
|
1138
|
+
return formFieldsFor(register).filter((f) => f.required && !String(draft[f.key] ?? "").trim()).map((f) => f.label);
|
|
1139
|
+
}
|
|
1140
|
+
function toCreatePayload(register, draft) {
|
|
1141
|
+
const out = {};
|
|
1142
|
+
for (const f of formFieldsFor(register)) {
|
|
1143
|
+
const raw = String(draft[f.key] ?? "").trim();
|
|
1144
|
+
if (raw === "") continue;
|
|
1145
|
+
if (f.kind === "number" || f.coerce === "number") {
|
|
1146
|
+
const n = Number(raw);
|
|
1147
|
+
if (!Number.isNaN(n)) out[f.key] = n;
|
|
1148
|
+
} else {
|
|
1149
|
+
out[f.key] = raw;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return out;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/field-styles.ts
|
|
1156
|
+
var FIELD_LABEL_CLASS = "block text-xs font-medium uppercase tracking-wide text-neutral-500 dark:text-neutral-400";
|
|
1157
|
+
var FIELD_CONTROL_CLASS = "mt-1 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100";
|
|
1158
|
+
|
|
1159
|
+
// src/use-mutations.ts
|
|
1160
|
+
import { useCallback as useCallback2, useState as useState3 } from "react";
|
|
1161
|
+
async function postJson(url, body) {
|
|
1162
|
+
const res = await fetch(url, {
|
|
1163
|
+
method: "POST",
|
|
1164
|
+
headers: { "content-type": "application/json" },
|
|
1165
|
+
body: JSON.stringify(body)
|
|
1166
|
+
});
|
|
1167
|
+
const payload = await res.json().catch(() => null);
|
|
1168
|
+
if (!res.ok) {
|
|
1169
|
+
const message = payload?.message ?? payload?.error ?? `Request failed (${res.status})`;
|
|
1170
|
+
throw new Error(message);
|
|
1171
|
+
}
|
|
1172
|
+
return payload.data;
|
|
1173
|
+
}
|
|
1174
|
+
function useCreate(register, basePath = "/api") {
|
|
1175
|
+
const [saving, setSaving] = useState3(false);
|
|
1176
|
+
const [error, setError] = useState3(null);
|
|
1177
|
+
const create = useCallback2(
|
|
1178
|
+
async (data) => {
|
|
1179
|
+
setSaving(true);
|
|
1180
|
+
setError(null);
|
|
1181
|
+
try {
|
|
1182
|
+
return await postJson(`${basePath}/${register}`, data);
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
const message = e instanceof Error ? e.message : "Failed to create";
|
|
1185
|
+
setError(message);
|
|
1186
|
+
throw e;
|
|
1187
|
+
} finally {
|
|
1188
|
+
setSaving(false);
|
|
1189
|
+
}
|
|
1190
|
+
},
|
|
1191
|
+
[register, basePath]
|
|
1192
|
+
);
|
|
1193
|
+
return { create, saving, error };
|
|
1194
|
+
}
|
|
1195
|
+
function useLink(basePath = "/api") {
|
|
1196
|
+
const [linking, setLinking] = useState3(false);
|
|
1197
|
+
const [error, setError] = useState3(null);
|
|
1198
|
+
const link = useCallback2(
|
|
1199
|
+
async (args) => {
|
|
1200
|
+
setLinking(true);
|
|
1201
|
+
setError(null);
|
|
1202
|
+
try {
|
|
1203
|
+
await postJson(`${basePath}/link`, args);
|
|
1204
|
+
} catch (e) {
|
|
1205
|
+
const message = e instanceof Error ? e.message : "Failed to link";
|
|
1206
|
+
setError(message);
|
|
1207
|
+
throw e;
|
|
1208
|
+
} finally {
|
|
1209
|
+
setLinking(false);
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
[basePath]
|
|
1213
|
+
);
|
|
1214
|
+
return { link, linking, error };
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/record-form.tsx
|
|
1218
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1219
|
+
function RecordForm({
|
|
1220
|
+
register,
|
|
1221
|
+
basePath,
|
|
1222
|
+
onCreated,
|
|
1223
|
+
onCancel
|
|
1224
|
+
}) {
|
|
1225
|
+
const fields = useMemo(() => formFieldsFor(register), [register]);
|
|
1226
|
+
const [draft, setDraft] = useState4(
|
|
1227
|
+
() => emptyDraft(register)
|
|
1228
|
+
);
|
|
1229
|
+
const { create, saving, error } = useCreate(register, basePath);
|
|
1230
|
+
const missing = missingRequired(register, draft);
|
|
1231
|
+
const set = (key, value) => setDraft((d) => ({ ...d, [key]: value }));
|
|
1232
|
+
const onSubmit = async (e) => {
|
|
1233
|
+
e.preventDefault();
|
|
1234
|
+
if (missing.length > 0 || saving) return;
|
|
1235
|
+
try {
|
|
1236
|
+
const created = await create(toCreatePayload(register, draft));
|
|
1237
|
+
onCreated(created.id);
|
|
1238
|
+
} catch {
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
return /* @__PURE__ */ jsxs6("form", { onSubmit, className: "flex h-full flex-col", children: [
|
|
1242
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex-1 space-y-4 overflow-y-auto p-5", children: [
|
|
1243
|
+
fields.map((field) => /* @__PURE__ */ jsx6(
|
|
1244
|
+
Field,
|
|
1245
|
+
{
|
|
1246
|
+
field,
|
|
1247
|
+
value: draft[field.key] ?? "",
|
|
1248
|
+
onChange: (v) => set(field.key, v)
|
|
1249
|
+
},
|
|
1250
|
+
field.key
|
|
1251
|
+
)),
|
|
1252
|
+
error ? /* @__PURE__ */ jsx6("p", { className: "text-sm text-red-600 dark:text-red-400", children: error }) : null
|
|
1253
|
+
] }),
|
|
1254
|
+
/* @__PURE__ */ jsxs6("footer", { className: "flex items-center justify-end gap-2 border-t border-neutral-200 p-5 dark:border-neutral-800", children: [
|
|
1255
|
+
/* @__PURE__ */ jsx6(
|
|
1256
|
+
"button",
|
|
1257
|
+
{
|
|
1258
|
+
type: "button",
|
|
1259
|
+
onClick: onCancel,
|
|
1260
|
+
className: "rounded-md px-3 py-2 text-sm text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900",
|
|
1261
|
+
children: "Cancel"
|
|
1262
|
+
}
|
|
1263
|
+
),
|
|
1264
|
+
/* @__PURE__ */ jsx6(
|
|
1265
|
+
"button",
|
|
1266
|
+
{
|
|
1267
|
+
type: "submit",
|
|
1268
|
+
disabled: missing.length > 0 || saving,
|
|
1269
|
+
title: missing.length > 0 ? `Fill in: ${missing.join(", ")}` : void 0,
|
|
1270
|
+
className: "rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50",
|
|
1271
|
+
children: saving ? "Creating\u2026" : `Create ${REGISTER_LABEL[register]}`
|
|
1272
|
+
}
|
|
1273
|
+
)
|
|
1274
|
+
] })
|
|
1275
|
+
] });
|
|
1276
|
+
}
|
|
1277
|
+
function Field({
|
|
1278
|
+
field,
|
|
1279
|
+
value,
|
|
1280
|
+
onChange
|
|
1281
|
+
}) {
|
|
1282
|
+
const id = `field-${field.key}`;
|
|
1283
|
+
return /* @__PURE__ */ jsxs6("div", { children: [
|
|
1284
|
+
/* @__PURE__ */ jsxs6("label", { htmlFor: id, className: FIELD_LABEL_CLASS, children: [
|
|
1285
|
+
field.label,
|
|
1286
|
+
field.required ? /* @__PURE__ */ jsx6("span", { className: "text-red-500", children: " *" }) : null
|
|
1287
|
+
] }),
|
|
1288
|
+
field.kind === "textarea" ? /* @__PURE__ */ jsx6(
|
|
1289
|
+
"textarea",
|
|
1290
|
+
{
|
|
1291
|
+
id,
|
|
1292
|
+
value,
|
|
1293
|
+
rows: 3,
|
|
1294
|
+
placeholder: field.placeholder,
|
|
1295
|
+
onChange: (e) => onChange(e.target.value),
|
|
1296
|
+
className: FIELD_CONTROL_CLASS
|
|
1297
|
+
}
|
|
1298
|
+
) : field.kind === "select" ? /* @__PURE__ */ jsxs6(
|
|
1299
|
+
"select",
|
|
1300
|
+
{
|
|
1301
|
+
id,
|
|
1302
|
+
value,
|
|
1303
|
+
onChange: (e) => onChange(e.target.value),
|
|
1304
|
+
className: FIELD_CONTROL_CLASS,
|
|
1305
|
+
children: [
|
|
1306
|
+
/* @__PURE__ */ jsx6("option", { value: "", children: "\u2014" }),
|
|
1307
|
+
field.options?.map((opt) => /* @__PURE__ */ jsx6("option", { value: opt, children: opt }, opt))
|
|
1308
|
+
]
|
|
1309
|
+
}
|
|
1310
|
+
) : /* @__PURE__ */ jsx6(
|
|
1311
|
+
"input",
|
|
1312
|
+
{
|
|
1313
|
+
id,
|
|
1314
|
+
type: field.kind === "number" ? "number" : "text",
|
|
1315
|
+
value,
|
|
1316
|
+
placeholder: field.placeholder,
|
|
1317
|
+
onChange: (e) => onChange(e.target.value),
|
|
1318
|
+
className: FIELD_CONTROL_CLASS
|
|
1319
|
+
}
|
|
1320
|
+
)
|
|
1321
|
+
] });
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// src/relation-editor.tsx
|
|
1325
|
+
import { useMemo as useMemo2, useState as useState5 } from "react";
|
|
1326
|
+
|
|
1327
|
+
// src/link-choices.ts
|
|
1328
|
+
import { RELATIONS } from "@validation-os/core";
|
|
1329
|
+
var RELATION_LABEL = {
|
|
1330
|
+
"assumption-reading": "Add reading",
|
|
1331
|
+
"assumption-depends-on": "Depends on",
|
|
1332
|
+
"assumption-contradicts": "Contradicts",
|
|
1333
|
+
"reading-experiment": "From experiment",
|
|
1334
|
+
"reading-goal": "From goal",
|
|
1335
|
+
"decision-based-on": "Based on assumption",
|
|
1336
|
+
"decision-resolves": "Resolves assumption",
|
|
1337
|
+
"goal-based-on": "Based on assumption"
|
|
1338
|
+
};
|
|
1339
|
+
function linkChoicesFrom(register) {
|
|
1340
|
+
return Object.keys(RELATIONS).filter((r) => RELATIONS[r].from.register === register).map((relation) => ({
|
|
1341
|
+
relation,
|
|
1342
|
+
label: RELATION_LABEL[relation],
|
|
1343
|
+
targetRegister: RELATIONS[relation].targetRegister
|
|
1344
|
+
}));
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/relation-editor.tsx
|
|
1348
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1349
|
+
function RelationEditor({
|
|
1350
|
+
register,
|
|
1351
|
+
recordId,
|
|
1352
|
+
basePath,
|
|
1353
|
+
onLinked
|
|
1354
|
+
}) {
|
|
1355
|
+
const choices = useMemo2(() => linkChoicesFrom(register), [register]);
|
|
1356
|
+
const [relation, setRelation] = useState5("");
|
|
1357
|
+
const [targetId, setTargetId] = useState5("");
|
|
1358
|
+
const { link, linking, error } = useLink(basePath);
|
|
1359
|
+
const active = choices.find((c) => c.relation === relation) ?? null;
|
|
1360
|
+
const { records } = useList(
|
|
1361
|
+
active?.targetRegister ?? register,
|
|
1362
|
+
basePath
|
|
1363
|
+
);
|
|
1364
|
+
const targets = (records ?? []).filter(
|
|
1365
|
+
(r) => !(active?.targetRegister === register && r.id === recordId)
|
|
1366
|
+
);
|
|
1367
|
+
if (choices.length === 0) return null;
|
|
1368
|
+
const onLink = async () => {
|
|
1369
|
+
if (!active || !targetId || linking) return;
|
|
1370
|
+
try {
|
|
1371
|
+
await link({
|
|
1372
|
+
relation: active.relation,
|
|
1373
|
+
from: { register, id: recordId },
|
|
1374
|
+
to: { register: active.targetRegister, id: targetId }
|
|
1375
|
+
});
|
|
1376
|
+
setTargetId("");
|
|
1377
|
+
setRelation("");
|
|
1378
|
+
onLinked();
|
|
1379
|
+
} catch {
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
return /* @__PURE__ */ jsxs7("section", { className: "border-t border-neutral-200 p-5 dark:border-neutral-800", children: [
|
|
1383
|
+
/* @__PURE__ */ jsx7("h3", { className: "mb-2 text-xs font-medium uppercase tracking-wide text-neutral-400", children: "Link a record" }),
|
|
1384
|
+
/* @__PURE__ */ jsxs7("div", { className: "space-y-3", children: [
|
|
1385
|
+
/* @__PURE__ */ jsxs7(
|
|
1386
|
+
"select",
|
|
1387
|
+
{
|
|
1388
|
+
"aria-label": "Relation",
|
|
1389
|
+
value: relation,
|
|
1390
|
+
onChange: (e) => {
|
|
1391
|
+
setRelation(e.target.value);
|
|
1392
|
+
setTargetId("");
|
|
1393
|
+
},
|
|
1394
|
+
className: FIELD_CONTROL_CLASS,
|
|
1395
|
+
children: [
|
|
1396
|
+
/* @__PURE__ */ jsx7("option", { value: "", children: "Choose a relation\u2026" }),
|
|
1397
|
+
choices.map((c) => /* @__PURE__ */ jsx7("option", { value: c.relation, children: c.label }, c.relation))
|
|
1398
|
+
]
|
|
1399
|
+
}
|
|
1400
|
+
),
|
|
1401
|
+
active ? /* @__PURE__ */ jsxs7(
|
|
1402
|
+
"select",
|
|
1403
|
+
{
|
|
1404
|
+
"aria-label": "Target record",
|
|
1405
|
+
value: targetId,
|
|
1406
|
+
onChange: (e) => setTargetId(e.target.value),
|
|
1407
|
+
className: FIELD_CONTROL_CLASS,
|
|
1408
|
+
children: [
|
|
1409
|
+
/* @__PURE__ */ jsx7("option", { value: "", children: "Choose a record\u2026" }),
|
|
1410
|
+
targets.map((r) => /* @__PURE__ */ jsx7("option", { value: r.id, children: primaryLabel(r) }, r.id))
|
|
1411
|
+
]
|
|
1412
|
+
}
|
|
1413
|
+
) : null,
|
|
1414
|
+
error ? /* @__PURE__ */ jsx7("p", { className: "text-sm text-red-600 dark:text-red-400", children: error }) : null,
|
|
1415
|
+
/* @__PURE__ */ jsx7(
|
|
1416
|
+
"button",
|
|
1417
|
+
{
|
|
1418
|
+
type: "button",
|
|
1419
|
+
onClick: onLink,
|
|
1420
|
+
disabled: !active || !targetId || linking,
|
|
1421
|
+
className: "rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50",
|
|
1422
|
+
children: linking ? "Linking\u2026" : "Link"
|
|
1423
|
+
}
|
|
1424
|
+
)
|
|
1425
|
+
] })
|
|
1426
|
+
] });
|
|
378
1427
|
}
|
|
379
1428
|
|
|
380
1429
|
// src/register-browser.tsx
|
|
381
|
-
import {
|
|
1430
|
+
import { useState as useState6 } from "react";
|
|
1431
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
382
1432
|
function RegisterBrowser({ register, basePath }) {
|
|
383
|
-
const { records, loading, error } = useList(
|
|
384
|
-
|
|
1433
|
+
const { records, loading, error, refresh: refreshList } = useList(
|
|
1434
|
+
register,
|
|
1435
|
+
basePath
|
|
1436
|
+
);
|
|
1437
|
+
const [openId, setOpenId] = useState6(null);
|
|
1438
|
+
const [creating, setCreating] = useState6(false);
|
|
385
1439
|
const {
|
|
386
1440
|
record,
|
|
387
1441
|
loading: recordLoading,
|
|
388
|
-
error: recordError
|
|
1442
|
+
error: recordError,
|
|
1443
|
+
refresh: refreshRecord
|
|
389
1444
|
} = useRecord(register, openId, basePath);
|
|
390
|
-
return /* @__PURE__ */
|
|
391
|
-
|
|
1445
|
+
return /* @__PURE__ */ jsxs8("div", { children: [
|
|
1446
|
+
/* @__PURE__ */ jsxs8("div", { className: "mb-3 flex items-center justify-between", children: [
|
|
1447
|
+
/* @__PURE__ */ jsx8("h1", { className: "text-lg font-semibold text-neutral-900 dark:text-neutral-50", children: REGISTER_LABEL[register] }),
|
|
1448
|
+
/* @__PURE__ */ jsxs8(
|
|
1449
|
+
"button",
|
|
1450
|
+
{
|
|
1451
|
+
type: "button",
|
|
1452
|
+
onClick: () => setCreating(true),
|
|
1453
|
+
className: "rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-500",
|
|
1454
|
+
children: [
|
|
1455
|
+
"New ",
|
|
1456
|
+
REGISTER_SINGULAR[register]
|
|
1457
|
+
]
|
|
1458
|
+
}
|
|
1459
|
+
)
|
|
1460
|
+
] }),
|
|
1461
|
+
loading && !records ? /* @__PURE__ */ jsxs8("p", { className: "text-sm text-neutral-500", children: [
|
|
392
1462
|
"Loading ",
|
|
393
1463
|
REGISTER_LABEL[register].toLowerCase(),
|
|
394
1464
|
"\u2026"
|
|
395
|
-
] }) : error ? /* @__PURE__ */
|
|
1465
|
+
] }) : error ? /* @__PURE__ */ jsx8("p", { className: "text-sm text-red-600 dark:text-red-400", children: error }) : /* @__PURE__ */ jsx8(
|
|
396
1466
|
RegisterTable,
|
|
397
1467
|
{
|
|
398
1468
|
register,
|
|
@@ -401,7 +1471,7 @@ function RegisterBrowser({ register, basePath }) {
|
|
|
401
1471
|
selectedId: openId
|
|
402
1472
|
}
|
|
403
1473
|
),
|
|
404
|
-
/* @__PURE__ */
|
|
1474
|
+
/* @__PURE__ */ jsx8(
|
|
405
1475
|
RecordDrawer,
|
|
406
1476
|
{
|
|
407
1477
|
register,
|
|
@@ -409,20 +1479,65 @@ function RegisterBrowser({ register, basePath }) {
|
|
|
409
1479
|
loading: recordLoading,
|
|
410
1480
|
error: recordError,
|
|
411
1481
|
open: openId !== null,
|
|
412
|
-
onClose: () => setOpenId(null)
|
|
1482
|
+
onClose: () => setOpenId(null),
|
|
1483
|
+
basePath,
|
|
1484
|
+
onChanged: () => {
|
|
1485
|
+
refreshRecord();
|
|
1486
|
+
refreshList();
|
|
1487
|
+
},
|
|
1488
|
+
children: openId ? /* @__PURE__ */ jsx8(
|
|
1489
|
+
RelationEditor,
|
|
1490
|
+
{
|
|
1491
|
+
register,
|
|
1492
|
+
recordId: openId,
|
|
1493
|
+
basePath,
|
|
1494
|
+
onLinked: () => {
|
|
1495
|
+
refreshRecord();
|
|
1496
|
+
refreshList();
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
) : null
|
|
1500
|
+
}
|
|
1501
|
+
),
|
|
1502
|
+
/* @__PURE__ */ jsxs8(
|
|
1503
|
+
DrawerShell,
|
|
1504
|
+
{
|
|
1505
|
+
open: creating,
|
|
1506
|
+
onClose: () => setCreating(false),
|
|
1507
|
+
ariaLabel: `New ${REGISTER_SINGULAR[register]} record`,
|
|
1508
|
+
scroll: false,
|
|
1509
|
+
children: [
|
|
1510
|
+
/* @__PURE__ */ jsxs8("header", { className: "border-b border-neutral-200 p-5 dark:border-neutral-800", children: [
|
|
1511
|
+
/* @__PURE__ */ jsx8("p", { className: "text-xs font-medium uppercase tracking-wide text-neutral-400", children: "New" }),
|
|
1512
|
+
/* @__PURE__ */ jsx8("h2", { className: "mt-1 text-lg font-semibold text-neutral-900 dark:text-neutral-50", children: REGISTER_SINGULAR[register] })
|
|
1513
|
+
] }),
|
|
1514
|
+
/* @__PURE__ */ jsx8(
|
|
1515
|
+
RecordForm,
|
|
1516
|
+
{
|
|
1517
|
+
register,
|
|
1518
|
+
basePath,
|
|
1519
|
+
onCreated: (id) => {
|
|
1520
|
+
setCreating(false);
|
|
1521
|
+
refreshList();
|
|
1522
|
+
setOpenId(id);
|
|
1523
|
+
},
|
|
1524
|
+
onCancel: () => setCreating(false)
|
|
1525
|
+
}
|
|
1526
|
+
)
|
|
1527
|
+
]
|
|
413
1528
|
}
|
|
414
1529
|
)
|
|
415
1530
|
] });
|
|
416
1531
|
}
|
|
417
1532
|
|
|
418
1533
|
// src/use-counts.ts
|
|
419
|
-
import { useCallback as
|
|
1534
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useState as useState7 } from "react";
|
|
420
1535
|
function useCounts(basePath = "/api") {
|
|
421
|
-
const [counts, setCounts] =
|
|
422
|
-
const [loading, setLoading] =
|
|
423
|
-
const [error, setError] =
|
|
424
|
-
const [tick, setTick] =
|
|
425
|
-
|
|
1536
|
+
const [counts, setCounts] = useState7(null);
|
|
1537
|
+
const [loading, setLoading] = useState7(true);
|
|
1538
|
+
const [error, setError] = useState7(null);
|
|
1539
|
+
const [tick, setTick] = useState7(0);
|
|
1540
|
+
useEffect4(() => {
|
|
426
1541
|
let live = true;
|
|
427
1542
|
setLoading(true);
|
|
428
1543
|
fetch(`${basePath}/counts`).then(async (res) => {
|
|
@@ -442,22 +1557,41 @@ function useCounts(basePath = "/api") {
|
|
|
442
1557
|
live = false;
|
|
443
1558
|
};
|
|
444
1559
|
}, [basePath, tick]);
|
|
445
|
-
const refresh =
|
|
1560
|
+
const refresh = useCallback3(() => setTick((t2) => t2 + 1), []);
|
|
446
1561
|
return { counts, loading, error, refresh };
|
|
447
1562
|
}
|
|
448
1563
|
export {
|
|
1564
|
+
CONFLICT_MESSAGE,
|
|
449
1565
|
REGISTER_LABEL,
|
|
450
1566
|
REGISTER_ORDER,
|
|
1567
|
+
REGISTER_SINGULAR,
|
|
451
1568
|
RecordDrawer,
|
|
1569
|
+
RecordForm,
|
|
452
1570
|
RegisterBrowser,
|
|
453
1571
|
RegisterCounts,
|
|
454
1572
|
RegisterTable,
|
|
1573
|
+
RelationEditor,
|
|
1574
|
+
UnderstandingPanel,
|
|
1575
|
+
buildPatch,
|
|
1576
|
+
buildUnderstanding,
|
|
455
1577
|
cellValue,
|
|
456
1578
|
columnsFor,
|
|
1579
|
+
draftFrom,
|
|
1580
|
+
editableFields,
|
|
1581
|
+
emptyDraft,
|
|
1582
|
+
formFieldsFor,
|
|
457
1583
|
formatValue,
|
|
1584
|
+
hasEdits,
|
|
1585
|
+
interpretSave,
|
|
1586
|
+
linkChoicesFrom,
|
|
1587
|
+
missingRequired,
|
|
458
1588
|
primaryLabel,
|
|
1589
|
+
toCreatePayload,
|
|
459
1590
|
useCounts,
|
|
1591
|
+
useCreate,
|
|
1592
|
+
useLink,
|
|
460
1593
|
useList,
|
|
461
|
-
useRecord
|
|
1594
|
+
useRecord,
|
|
1595
|
+
useUpdate
|
|
462
1596
|
};
|
|
463
1597
|
//# sourceMappingURL=index.js.map
|