@joewinke/jatui 0.37.0 → 0.38.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/artifact-blocks/AttachmentChip.svelte +66 -0
- package/dist/artifact-blocks/AttachmentChip.svelte.d.ts +7 -0
- package/dist/artifact-blocks/BlockComments.svelte +309 -0
- package/dist/artifact-blocks/BlockComments.svelte.d.ts +33 -0
- package/dist/artifact-blocks/BlockRenderer.svelte +674 -0
- package/dist/artifact-blocks/BlockRenderer.svelte.d.ts +55 -0
- package/dist/artifact-blocks/BlockVerdict.svelte +151 -0
- package/dist/artifact-blocks/BlockVerdict.svelte.d.ts +13 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte +1616 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte.d.ts +113 -0
- package/dist/artifact-blocks/CONTRACT.md +225 -0
- package/dist/artifact-blocks/CommentPill.svelte +49 -0
- package/dist/artifact-blocks/CommentPill.svelte.d.ts +16 -0
- package/dist/artifact-blocks/FoldedRounds.svelte +95 -0
- package/dist/artifact-blocks/FoldedRounds.svelte.d.ts +9 -0
- package/dist/artifact-blocks/Glyph.svelte +54 -0
- package/dist/artifact-blocks/Glyph.svelte.d.ts +10 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte +500 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte.d.ts +15 -0
- package/dist/artifact-blocks/Markdown.svelte +186 -0
- package/dist/artifact-blocks/Markdown.svelte.d.ts +30 -0
- package/dist/artifact-blocks/NoteComposer.svelte +31 -0
- package/dist/artifact-blocks/NoteComposer.svelte.d.ts +8 -0
- package/dist/artifact-blocks/PlainComposer.svelte +200 -0
- package/dist/artifact-blocks/PlainComposer.svelte.d.ts +15 -0
- package/dist/artifact-blocks/SectionTools.svelte +102 -0
- package/dist/artifact-blocks/SectionTools.svelte.d.ts +19 -0
- package/dist/artifact-blocks/adapter.d.ts +13 -0
- package/dist/artifact-blocks/adapter.js +31 -0
- package/dist/artifact-blocks/autoSend.d.ts +49 -0
- package/dist/artifact-blocks/autoSend.js +103 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte +557 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte.d.ts +48 -0
- package/dist/artifact-blocks/blocks/actions/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/actions/block.js +74 -0
- package/dist/artifact-blocks/blocks/actions/requests.d.ts +62 -0
- package/dist/artifact-blocks/blocks/actions/requests.js +26 -0
- package/dist/artifact-blocks/blocks/actions/schema.d.ts +168 -0
- package/dist/artifact-blocks/blocks/actions/schema.js +253 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte +779 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte.d.ts +40 -0
- package/dist/artifact-blocks/blocks/callout/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/callout/block.js +39 -0
- package/dist/artifact-blocks/blocks/callout/schema.d.ts +72 -0
- package/dist/artifact-blocks/blocks/callout/schema.js +115 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte +875 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte.d.ts +59 -0
- package/dist/artifact-blocks/blocks/chart/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/chart/block.js +70 -0
- package/dist/artifact-blocks/blocks/chart/schema.d.ts +171 -0
- package/dist/artifact-blocks/blocks/chart/schema.js +498 -0
- package/dist/artifact-blocks/blocks/chart/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/chart/thumb.js +201 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte +1048 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte +105 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/blocks/checklist/block.d.ts +10 -0
- package/dist/artifact-blocks/blocks/checklist/block.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/labels.d.ts +62 -0
- package/dist/artifact-blocks/blocks/checklist/labels.js +96 -0
- package/dist/artifact-blocks/blocks/checklist/retest.d.ts +43 -0
- package/dist/artifact-blocks/blocks/checklist/retest.js +68 -0
- package/dist/artifact-blocks/blocks/checklist/schema.d.ts +84 -0
- package/dist/artifact-blocks/blocks/checklist/schema.js +91 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte +128 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte.d.ts +12 -0
- package/dist/artifact-blocks/blocks/code/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/code/block.js +31 -0
- package/dist/artifact-blocks/blocks/code/highlight.d.ts +34 -0
- package/dist/artifact-blocks/blocks/code/highlight.js +280 -0
- package/dist/artifact-blocks/blocks/code/schema.d.ts +32 -0
- package/dist/artifact-blocks/blocks/code/schema.js +39 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte +290 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte.d.ts +29 -0
- package/dist/artifact-blocks/blocks/collage/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/collage/block.js +29 -0
- package/dist/artifact-blocks/blocks/collage/schema.d.ts +52 -0
- package/dist/artifact-blocks/blocks/collage/schema.js +86 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte +484 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte.d.ts +25 -0
- package/dist/artifact-blocks/blocks/compare/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/compare/block.js +32 -0
- package/dist/artifact-blocks/blocks/compare/schema.d.ts +74 -0
- package/dist/artifact-blocks/blocks/compare/schema.js +148 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte +392 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +30 -0
- package/dist/artifact-blocks/blocks/diagram/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/diagram/block.js +50 -0
- package/dist/artifact-blocks/blocks/diagram/constants.d.ts +48 -0
- package/dist/artifact-blocks/blocks/diagram/constants.js +53 -0
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +161 -0
- package/dist/artifact-blocks/blocks/diagram/schema.js +621 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.js +157 -0
- package/dist/artifact-blocks/blocks/diagram/types.d.ts +74 -0
- package/dist/artifact-blocks/blocks/diagram/types.js +1 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte +81 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte.d.ts +10 -0
- package/dist/artifact-blocks/blocks/diff/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/diff/block.js +36 -0
- package/dist/artifact-blocks/blocks/diff/schema.d.ts +18 -0
- package/dist/artifact-blocks/blocks/diff/schema.js +35 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte +1055 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/diff-review/block.d.ts +15 -0
- package/dist/artifact-blocks/blocks/diff-review/block.js +64 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.d.ts +57 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.js +80 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.d.ts +13 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.js +84 -0
- package/dist/artifact-blocks/blocks/diff-review/types.d.ts +35 -0
- package/dist/artifact-blocks/blocks/diff-review/types.js +1 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte +145 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/explainer/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/explainer/block.js +21 -0
- package/dist/artifact-blocks/blocks/explainer/schema.d.ts +26 -0
- package/dist/artifact-blocks/blocks/explainer/schema.js +69 -0
- package/dist/artifact-blocks/blocks/group-content.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-content.js +16 -0
- package/dist/artifact-blocks/blocks/group-data.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-data.js +7 -0
- package/dist/artifact-blocks/blocks/group-review.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-review.js +12 -0
- package/dist/artifact-blocks/blocks/group-visual.d.ts +17 -0
- package/dist/artifact-blocks/blocks/group-visual.js +14 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte +413 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte.d.ts +53 -0
- package/dist/artifact-blocks/blocks/map/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/map/block.js +62 -0
- package/dist/artifact-blocks/blocks/map/schema.d.ts +123 -0
- package/dist/artifact-blocks/blocks/map/schema.js +113 -0
- package/dist/artifact-blocks/blocks/map/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/map/thumb.js +69 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte +33 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte.d.ts +15 -0
- package/dist/artifact-blocks/blocks/markdown/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/markdown/block.js +28 -0
- package/dist/artifact-blocks/blocks/markdown/render.d.ts +17 -0
- package/dist/artifact-blocks/blocks/markdown/render.js +115 -0
- package/dist/artifact-blocks/blocks/markdown/schema.d.ts +10 -0
- package/dist/artifact-blocks/blocks/markdown/schema.js +23 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte +2112 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/media-picker/block.d.ts +24 -0
- package/dist/artifact-blocks/blocks/media-picker/block.js +137 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.d.ts +28 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.js +35 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.js +75 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.js +199 -0
- package/dist/artifact-blocks/blocks/media-picker/types.d.ts +92 -0
- package/dist/artifact-blocks/blocks/media-picker/types.js +1 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.js +118 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte +213 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/mockup/block.d.ts +26 -0
- package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
- package/dist/artifact-blocks/blocks/mockup/doc.d.ts +24 -0
- package/dist/artifact-blocks/blocks/mockup/doc.js +44 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +21 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.js +27 -0
- package/dist/artifact-blocks/blocks/mockup/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/mockup/schema.js +90 -0
- package/dist/artifact-blocks/blocks/mockup/types.d.ts +36 -0
- package/dist/artifact-blocks/blocks/mockup/types.js +17 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte +359 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte.d.ts +32 -0
- package/dist/artifact-blocks/blocks/program/block.d.ts +35 -0
- package/dist/artifact-blocks/blocks/program/block.js +68 -0
- package/dist/artifact-blocks/blocks/program/schema.d.ts +132 -0
- package/dist/artifact-blocks/blocks/program/schema.js +268 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte +315 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte.d.ts +24 -0
- package/dist/artifact-blocks/blocks/stats/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/stats/block.js +49 -0
- package/dist/artifact-blocks/blocks/stats/schema.d.ts +82 -0
- package/dist/artifact-blocks/blocks/stats/schema.js +43 -0
- package/dist/artifact-blocks/blocks/stats/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/stats/thumb.js +102 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte +490 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte.d.ts +28 -0
- package/dist/artifact-blocks/blocks/table/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/table/block.js +100 -0
- package/dist/artifact-blocks/blocks/table/schema.d.ts +143 -0
- package/dist/artifact-blocks/blocks/table/schema.js +278 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte +347 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte.d.ts +42 -0
- package/dist/artifact-blocks/blocks/timeline/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/timeline/block.js +96 -0
- package/dist/artifact-blocks/blocks/timeline/constants.d.ts +26 -0
- package/dist/artifact-blocks/blocks/timeline/constants.js +27 -0
- package/dist/artifact-blocks/blocks/timeline/schema.d.ts +102 -0
- package/dist/artifact-blocks/blocks/timeline/schema.js +405 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.js +117 -0
- package/dist/artifact-blocks/blocks/timeline/types.d.ts +66 -0
- package/dist/artifact-blocks/blocks/timeline/types.js +1 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte +169 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte.d.ts +26 -0
- package/dist/artifact-blocks/blocks/transcript/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/transcript/block.js +86 -0
- package/dist/artifact-blocks/blocks/transcript/schema.d.ts +114 -0
- package/dist/artifact-blocks/blocks/transcript/schema.js +126 -0
- package/dist/artifact-blocks/builtins.d.ts +40 -0
- package/dist/artifact-blocks/builtins.js +22 -0
- package/dist/artifact-blocks/contractBlocks.d.ts +92 -0
- package/dist/artifact-blocks/contractBlocks.js +279 -0
- package/dist/artifact-blocks/history.d.ts +140 -0
- package/dist/artifact-blocks/history.js +363 -0
- package/dist/artifact-blocks/index.d.ts +40 -0
- package/dist/artifact-blocks/index.js +46 -0
- package/dist/artifact-blocks/listen/context.d.ts +30 -0
- package/dist/artifact-blocks/listen/context.js +30 -0
- package/dist/artifact-blocks/liveData.d.ts +78 -0
- package/dist/artifact-blocks/liveData.js +137 -0
- package/dist/artifact-blocks/liveDataPoller.d.ts +41 -0
- package/dist/artifact-blocks/liveDataPoller.js +140 -0
- package/dist/artifact-blocks/openNav.d.ts +46 -0
- package/dist/artifact-blocks/openNav.js +106 -0
- package/dist/artifact-blocks/pageReviewKeys.d.ts +17 -0
- package/dist/artifact-blocks/pageReviewKeys.js +115 -0
- package/dist/artifact-blocks/params.d.ts +98 -0
- package/dist/artifact-blocks/params.js +159 -0
- package/dist/artifact-blocks/registry.d.ts +27 -0
- package/dist/artifact-blocks/registry.js +83 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte +99 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte +94 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte.d.ts +11 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte +40 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte.d.ts +9 -0
- package/dist/artifact-blocks/review/ItemNote.svelte +193 -0
- package/dist/artifact-blocks/review/ItemNote.svelte.d.ts +33 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte +52 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte.d.ts +12 -0
- package/dist/artifact-blocks/review/TeamChip.svelte +71 -0
- package/dist/artifact-blocks/review/TeamChip.svelte.d.ts +8 -0
- package/dist/artifact-blocks/review/attachments.d.ts +10 -0
- package/dist/artifact-blocks/review/attachments.js +37 -0
- package/dist/artifact-blocks/review/bulk.d.ts +52 -0
- package/dist/artifact-blocks/review/bulk.js +61 -0
- package/dist/artifact-blocks/review/itemReview.d.ts +33 -0
- package/dist/artifact-blocks/review/itemReview.js +112 -0
- package/dist/artifact-blocks/review/roundLines.d.ts +14 -0
- package/dist/artifact-blocks/review/roundLines.js +20 -0
- package/dist/artifact-blocks/review/rounds.d.ts +86 -0
- package/dist/artifact-blocks/review/rounds.js +302 -0
- package/dist/artifact-blocks/review/saver.d.ts +23 -0
- package/dist/artifact-blocks/review/saver.js +48 -0
- package/dist/artifact-blocks/review/team.d.ts +31 -0
- package/dist/artifact-blocks/review/team.js +42 -0
- package/dist/artifact-blocks/review/visualOrder.d.ts +29 -0
- package/dist/artifact-blocks/review/visualOrder.js +66 -0
- package/dist/artifact-blocks/review.d.ts +180 -0
- package/dist/artifact-blocks/review.js +716 -0
- package/dist/artifact-blocks/thumb-kit.d.ts +80 -0
- package/dist/artifact-blocks/thumb-kit.js +149 -0
- package/dist/artifact-blocks/types.d.ts +513 -0
- package/dist/artifact-blocks/types.js +1 -0
- package/package.json +11 -2
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review math of a blocks page (ported from MOS
|
|
3
|
+
* modules/artifacts/feedbackReview.ts, jatui-vsuzi) — the census, the
|
|
4
|
+
* still-open walk, the verdicts a Send carries and the changed-after-send
|
|
5
|
+
* diff. PURE and isomorphic: the view reads it in the browser, and a host
|
|
6
|
+
* server may run the same functions over a stored spec so a desk Send and a
|
|
7
|
+
* page Send answer identically.
|
|
8
|
+
*
|
|
9
|
+
* What stays HOST-side (not here): validating an untrusted review body,
|
|
10
|
+
* composing the answer comment, routing it to the owner, delivery. Those
|
|
11
|
+
* need the host's store and its jat binding.
|
|
12
|
+
*
|
|
13
|
+
* Where per-block verdicts live between visits: the viewer's saved blocks
|
|
14
|
+
* map, under the reserved key `review:<blockId>` (reviewKey). Block ids are
|
|
15
|
+
* `[a-z0-9_-]`, so the colon can never collide with a block's own state.
|
|
16
|
+
*
|
|
17
|
+
* Item-review blocks (checklist, media-picker, diff-review) carry their
|
|
18
|
+
* verdicts PER ITEM inside their own state; the core reads them through the
|
|
19
|
+
* block def's `itemReview` hook (types.ts BlockItemReview), never by
|
|
20
|
+
* knowing the block's internals.
|
|
21
|
+
*/
|
|
22
|
+
import { blockDef } from "./registry.js";
|
|
23
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Specs
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* A stored spec, defensively: untrusted JSON → a BlocksSpec, or null when it
|
|
29
|
+
* is not one. NOT registry validation — rendering must degrade per block,
|
|
30
|
+
* not refuse the whole page. A JSON string (a double-encoded jsonb scalar)
|
|
31
|
+
* is decoded first.
|
|
32
|
+
*/
|
|
33
|
+
export function coerceBlocksSpec(input) {
|
|
34
|
+
let raw = input;
|
|
35
|
+
if (typeof raw === "string") {
|
|
36
|
+
try {
|
|
37
|
+
raw = JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!isObj(raw))
|
|
44
|
+
return null;
|
|
45
|
+
if (raw.version !== 1)
|
|
46
|
+
return null;
|
|
47
|
+
if (typeof raw.title !== "string")
|
|
48
|
+
return null;
|
|
49
|
+
if (!Array.isArray(raw.blocks))
|
|
50
|
+
return null;
|
|
51
|
+
const blocks = [];
|
|
52
|
+
for (const b of raw.blocks) {
|
|
53
|
+
if (!isObj(b))
|
|
54
|
+
return null;
|
|
55
|
+
if (typeof b.id !== "string" || typeof b.type !== "string")
|
|
56
|
+
return null;
|
|
57
|
+
const inst = { id: b.id, type: b.type, params: b.params ?? {} };
|
|
58
|
+
if (typeof b.owner === "string")
|
|
59
|
+
inst.owner = b.owner;
|
|
60
|
+
if (typeof b.taskId === "string")
|
|
61
|
+
inst.taskId = b.taskId;
|
|
62
|
+
if (typeof b.decisionId === "string")
|
|
63
|
+
inst.decisionId = b.decisionId;
|
|
64
|
+
blocks.push(inst);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
version: 1,
|
|
68
|
+
title: raw.title,
|
|
69
|
+
summary: typeof raw.summary === "string" ? raw.summary : undefined,
|
|
70
|
+
blocks,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Which blocks are review units
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/** Reserved blocks-map key for a block's chrome verdict. */
|
|
77
|
+
const REVIEW_KEY_PREFIX = "review:";
|
|
78
|
+
export function reviewKey(blockId) {
|
|
79
|
+
return `${REVIEW_KEY_PREFIX}${blockId}`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Block types whose verdicts are per ITEM inside their own state. Known by
|
|
83
|
+
* name so the census holds even before a host registers the block (it then
|
|
84
|
+
* counts zero items and never offers a block-level verdict); a registered
|
|
85
|
+
* block with an `itemReview` hook joins automatically.
|
|
86
|
+
*/
|
|
87
|
+
export const ITEM_REVIEW_BLOCK_TYPES = new Set([
|
|
88
|
+
"media-picker",
|
|
89
|
+
"diff-review",
|
|
90
|
+
"checklist",
|
|
91
|
+
]);
|
|
92
|
+
export function isItemReviewBlock(type) {
|
|
93
|
+
return ITEM_REVIEW_BLOCK_TYPES.has(type) || Boolean(blockDef(type)?.itemReview);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Block types that present a WORK PRODUCT to judge and so take the
|
|
97
|
+
* block-level approve / needs-a-pass strip by default. Every other ordinary
|
|
98
|
+
* block — stats, table, chart, markdown, a plain callout, a map… — is
|
|
99
|
+
* INFORMATION: no strip, not a review unit. A registered def can join with
|
|
100
|
+
* `reviewableByDefault: true`.
|
|
101
|
+
*/
|
|
102
|
+
const REVIEWABLE_BY_DEFAULT = new Set(["mockup", "compare", "collage", "diff"]);
|
|
103
|
+
/** The id of a decision callout's free-text "Other" choice. */
|
|
104
|
+
export const OTHER_ID = "__other__";
|
|
105
|
+
/** A decision callout: answered by its own pick, never by a verdict strip. */
|
|
106
|
+
export function isDecisionBlock(type, params) {
|
|
107
|
+
return type === "callout" && isObj(params) && params.variant === "decision";
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Does this block take the block-level verdict strip (and count as one
|
|
111
|
+
* review unit)? Never an item-review block or a decision callout.
|
|
112
|
+
* Otherwise the spec decides with `params.reviewable: true|false` (a page
|
|
113
|
+
* intro sets `reviewable: false` explicitly — PARAMS-SPEC §3); absent that,
|
|
114
|
+
* only the work-product types do.
|
|
115
|
+
*/
|
|
116
|
+
export function takesBlockVerdict(type, params) {
|
|
117
|
+
if (isItemReviewBlock(type))
|
|
118
|
+
return false;
|
|
119
|
+
if (isDecisionBlock(type, params))
|
|
120
|
+
return false;
|
|
121
|
+
if (isObj(params) && typeof params.reviewable === "boolean")
|
|
122
|
+
return params.reviewable;
|
|
123
|
+
return (REVIEWABLE_BY_DEFAULT.has(type) ||
|
|
124
|
+
blockDef(type)?.reviewableByDefault === true);
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Decision cards
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
const OPTION_RE = /^[A-Z]$/;
|
|
130
|
+
/**
|
|
131
|
+
* The page's decision cards: `callout` blocks with variant `decision`.
|
|
132
|
+
* Answered with a LETTER: options get A, B, C… by position unless they
|
|
133
|
+
* carry their own, and an `allowOther` card's "Other" takes the next free
|
|
134
|
+
* letter.
|
|
135
|
+
*/
|
|
136
|
+
export function decisionCardsOf(spec) {
|
|
137
|
+
const out = new Map();
|
|
138
|
+
for (const b of coerceBlocksSpec(spec)?.blocks ?? []) {
|
|
139
|
+
if (b.type !== "callout" || !isObj(b.params))
|
|
140
|
+
continue;
|
|
141
|
+
const p = b.params;
|
|
142
|
+
if (p.variant !== "decision" || !Array.isArray(p.options))
|
|
143
|
+
continue;
|
|
144
|
+
const options = [];
|
|
145
|
+
for (const o of p.options) {
|
|
146
|
+
if (!isObj(o) || typeof o.id !== "string")
|
|
147
|
+
continue;
|
|
148
|
+
options.push({
|
|
149
|
+
letter: typeof o.letter === "string" && OPTION_RE.test(o.letter)
|
|
150
|
+
? o.letter
|
|
151
|
+
: String.fromCharCode(65 + options.length),
|
|
152
|
+
id: o.id,
|
|
153
|
+
label: typeof o.label === "string" ? o.label : o.id,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (p.allowOther === true) {
|
|
157
|
+
const used = new Set(options.map((o) => o.letter));
|
|
158
|
+
let code = 65 + options.length;
|
|
159
|
+
while (used.has(String.fromCharCode(code)) && code < 90)
|
|
160
|
+
code++;
|
|
161
|
+
options.push({
|
|
162
|
+
letter: String.fromCharCode(code),
|
|
163
|
+
id: OTHER_ID,
|
|
164
|
+
label: "Other",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
out.set(b.id, {
|
|
168
|
+
question: typeof p.question === "string"
|
|
169
|
+
? p.question
|
|
170
|
+
: typeof p.title === "string"
|
|
171
|
+
? p.title
|
|
172
|
+
: b.id,
|
|
173
|
+
options,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/** The letter a saved callout pick stands for, or null. */
|
|
179
|
+
function letterForPick(card, pick) {
|
|
180
|
+
if (typeof pick !== "string")
|
|
181
|
+
return null;
|
|
182
|
+
return card.options.find((o) => o.id === pick)?.letter ?? null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The contract verdicts a Send carries, read from a viewer's blocks map:
|
|
186
|
+
* each decision card's pick as `chose` + its letter, and each
|
|
187
|
+
* verdict-taking block's `review:<id>` entry. Item-review blocks carry
|
|
188
|
+
* their verdicts per item (itemVerdictsOf), never here.
|
|
189
|
+
*/
|
|
190
|
+
export function verdictsFromBlocks(spec, blocks) {
|
|
191
|
+
const out = {};
|
|
192
|
+
const parsed = coerceBlocksSpec(spec);
|
|
193
|
+
if (!parsed || !blocks)
|
|
194
|
+
return out;
|
|
195
|
+
const cards = decisionCardsOf(parsed);
|
|
196
|
+
for (const b of parsed.blocks) {
|
|
197
|
+
if (isItemReviewBlock(b.type))
|
|
198
|
+
continue;
|
|
199
|
+
const card = cards.get(b.id);
|
|
200
|
+
if (!card && !takesBlockVerdict(b.type, b.params))
|
|
201
|
+
continue;
|
|
202
|
+
if (card) {
|
|
203
|
+
const st = blocks[b.id];
|
|
204
|
+
const letter = isObj(st) ? letterForPick(card, st.pick) : null;
|
|
205
|
+
if (!letter)
|
|
206
|
+
continue;
|
|
207
|
+
const e = { verdict: "chose", option: letter };
|
|
208
|
+
if (isObj(st) && typeof st.comment === "string" && st.comment.trim())
|
|
209
|
+
e.note = st.comment.trim();
|
|
210
|
+
if (isObj(st) && Array.isArray(st.attachments) && st.attachments.length)
|
|
211
|
+
e.attachments = st.attachments;
|
|
212
|
+
out[b.id] = e;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const rv = blocks[reviewKey(b.id)];
|
|
216
|
+
if (!isObj(rv))
|
|
217
|
+
continue;
|
|
218
|
+
if (rv.verdict !== "approved" && rv.verdict !== "needs-pass")
|
|
219
|
+
continue;
|
|
220
|
+
const e = { verdict: rv.verdict };
|
|
221
|
+
if (typeof rv.note === "string" && rv.note.trim())
|
|
222
|
+
e.note = rv.note.trim();
|
|
223
|
+
if (Array.isArray(rv.attachments) && rv.attachments.length)
|
|
224
|
+
e.attachments = rv.attachments;
|
|
225
|
+
out[b.id] = e;
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
/** A saved chrome verdict, defensively. */
|
|
230
|
+
export function blockReviewStateOf(blocks, blockId) {
|
|
231
|
+
const s = blocks[reviewKey(blockId)];
|
|
232
|
+
return isObj(s) ? s : null;
|
|
233
|
+
}
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Items inside item-review blocks
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
/** The items of one item-review block (through its def's hook). Empty when
|
|
238
|
+
* the block is unregistered or declares no hook. */
|
|
239
|
+
export function reviewItemsOf(type, params, state) {
|
|
240
|
+
const hook = blockDef(type)?.itemReview;
|
|
241
|
+
if (!hook)
|
|
242
|
+
return [];
|
|
243
|
+
try {
|
|
244
|
+
return hook.items(params, state);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Untrusted stored JSON: a hook that trips over it counts nothing
|
|
248
|
+
// rather than taking the page down.
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** Every block-local word normalized to a bucket (the sent-summary line). */
|
|
253
|
+
const ITEM_VERDICTS = new Map([
|
|
254
|
+
["approve", "approved"],
|
|
255
|
+
["approved", "approved"],
|
|
256
|
+
["pass", "approved"],
|
|
257
|
+
["accept", "approved"],
|
|
258
|
+
["redo", "needs-work"],
|
|
259
|
+
["modify", "needs-work"],
|
|
260
|
+
["fail", "needs-work"],
|
|
261
|
+
["blocked", "needs-work"],
|
|
262
|
+
["needs-work", "needs-work"],
|
|
263
|
+
["needs-pass", "needs-work"],
|
|
264
|
+
["reject", "rejected"],
|
|
265
|
+
["rejected", "rejected"],
|
|
266
|
+
]);
|
|
267
|
+
/** The chrome vocabulary: per-block verdicts plus decision picks. */
|
|
268
|
+
export const BLOCKS_VOCABULARY = [
|
|
269
|
+
{ id: "approved", label: "Approve", tone: "success" },
|
|
270
|
+
{ id: "needs-pass", label: "Needs a pass", tone: "warning" },
|
|
271
|
+
{ id: "chose", label: "Chosen", tone: "info" },
|
|
272
|
+
];
|
|
273
|
+
const TONE_OF_BUCKET = {
|
|
274
|
+
approved: "success",
|
|
275
|
+
"needs-work": "warning",
|
|
276
|
+
"needs-pass": "warning",
|
|
277
|
+
rejected: "error",
|
|
278
|
+
chose: "info",
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* The word a stored verdict shows in `vocab`. `raw` is the block's own id;
|
|
282
|
+
* a spelling the vocabulary does not know falls back to the vocabulary's
|
|
283
|
+
* word for the same bucket. null = no word.
|
|
284
|
+
*/
|
|
285
|
+
export function wordFor(vocab, raw, bucket) {
|
|
286
|
+
const exact = raw ? vocab.find((w) => w.id === raw) : undefined;
|
|
287
|
+
if (exact)
|
|
288
|
+
return exact;
|
|
289
|
+
const tone = bucket ? TONE_OF_BUCKET[bucket] : undefined;
|
|
290
|
+
return (tone && vocab.find((w) => w.tone === tone)) || null;
|
|
291
|
+
}
|
|
292
|
+
/** An item-review block's vocabulary key + words. */
|
|
293
|
+
function vocabularyOf(type) {
|
|
294
|
+
const hook = blockDef(type)?.itemReview;
|
|
295
|
+
return hook
|
|
296
|
+
? { key: type, words: hook.vocabulary }
|
|
297
|
+
: { key: "blocks", words: BLOCKS_VOCABULARY };
|
|
298
|
+
}
|
|
299
|
+
/** A block's label in jump cards / progress lines. */
|
|
300
|
+
function itemBlockTitle(b) {
|
|
301
|
+
const p = isObj(b.params) ? b.params : {};
|
|
302
|
+
if (typeof p.title === "string" && p.title.trim())
|
|
303
|
+
return p.title.trim();
|
|
304
|
+
return blockDef(b.type)?.itemReview?.name ?? blockDef(b.type)?.title ?? b.type;
|
|
305
|
+
}
|
|
306
|
+
const bump = (counts, id) => {
|
|
307
|
+
counts[id] = (counts[id] ?? 0) + 1;
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* Count one viewer's reviewable units: every item of an item-review block,
|
|
311
|
+
* every decision callout, and the approve / needs-a-pass of every block that
|
|
312
|
+
* takes one. Information blocks are not units. `blocks` is the viewer's
|
|
313
|
+
* saved blocks map.
|
|
314
|
+
*/
|
|
315
|
+
export function countReviewUnits(spec, blocks) {
|
|
316
|
+
const c = {
|
|
317
|
+
total: 0,
|
|
318
|
+
approved: 0,
|
|
319
|
+
needsWork: 0,
|
|
320
|
+
rejected: 0,
|
|
321
|
+
chose: 0,
|
|
322
|
+
retest: 0,
|
|
323
|
+
itemBlocks: 0,
|
|
324
|
+
lines: [],
|
|
325
|
+
};
|
|
326
|
+
const cards = decisionCardsOf(spec);
|
|
327
|
+
let other = null;
|
|
328
|
+
for (const b of coerceBlocksSpec(spec)?.blocks ?? []) {
|
|
329
|
+
if (isItemReviewBlock(b.type)) {
|
|
330
|
+
c.itemBlocks++;
|
|
331
|
+
const vocab = vocabularyOf(b.type);
|
|
332
|
+
const line = {
|
|
333
|
+
key: b.id,
|
|
334
|
+
vocab: vocab.key,
|
|
335
|
+
words: vocab.words,
|
|
336
|
+
title: itemBlockTitle(b),
|
|
337
|
+
counts: {},
|
|
338
|
+
total: 0,
|
|
339
|
+
retest: 0,
|
|
340
|
+
};
|
|
341
|
+
c.lines.push(line);
|
|
342
|
+
for (const it of reviewItemsOf(b.type, b.params, blocks[b.id])) {
|
|
343
|
+
c.total++;
|
|
344
|
+
line.total++;
|
|
345
|
+
if (it.verdict === "approved")
|
|
346
|
+
c.approved++;
|
|
347
|
+
else if (it.verdict === "needs-work")
|
|
348
|
+
c.needsWork++;
|
|
349
|
+
else if (it.verdict === "rejected")
|
|
350
|
+
c.rejected++;
|
|
351
|
+
else if (it.retest) {
|
|
352
|
+
c.retest++;
|
|
353
|
+
line.retest++;
|
|
354
|
+
}
|
|
355
|
+
const word = it.verdict ? wordFor(vocab.words, it.raw, it.verdict) : null;
|
|
356
|
+
if (word)
|
|
357
|
+
bump(line.counts, word.id);
|
|
358
|
+
}
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const card = cards.get(b.id);
|
|
362
|
+
if (!card && !takesBlockVerdict(b.type, b.params))
|
|
363
|
+
continue;
|
|
364
|
+
c.total++;
|
|
365
|
+
if (!other) {
|
|
366
|
+
other = {
|
|
367
|
+
key: "blocks",
|
|
368
|
+
vocab: "blocks",
|
|
369
|
+
words: BLOCKS_VOCABULARY,
|
|
370
|
+
title: "Blocks",
|
|
371
|
+
counts: {},
|
|
372
|
+
total: 0,
|
|
373
|
+
retest: 0,
|
|
374
|
+
};
|
|
375
|
+
c.lines.push(other);
|
|
376
|
+
}
|
|
377
|
+
other.total++;
|
|
378
|
+
if (card) {
|
|
379
|
+
const st = blocks[b.id];
|
|
380
|
+
if (isObj(st) && letterForPick(card, st.pick)) {
|
|
381
|
+
c.chose++;
|
|
382
|
+
bump(other.counts, "chose");
|
|
383
|
+
}
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const rv = blocks[reviewKey(b.id)];
|
|
387
|
+
if (!isObj(rv))
|
|
388
|
+
continue;
|
|
389
|
+
if (rv.verdict === "approved")
|
|
390
|
+
c.approved++;
|
|
391
|
+
else if (rv.verdict === "needs-pass")
|
|
392
|
+
c.needsWork++;
|
|
393
|
+
else
|
|
394
|
+
continue;
|
|
395
|
+
bump(other.counts, rv.verdict);
|
|
396
|
+
}
|
|
397
|
+
return c;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* The Finish review bar's progress, aggregated by tone but LABELLED with
|
|
401
|
+
* the page's own words: one vocabulary → one sentence; mixed vocabularies
|
|
402
|
+
* → each block keeps its own sentence. Never folds one word into another.
|
|
403
|
+
*/
|
|
404
|
+
export function pageReviewProgress(c) {
|
|
405
|
+
const vocabs = new Set(c.lines.map((l) => l.vocab));
|
|
406
|
+
const decided = c.lines.reduce((n, l) => n + Object.values(l.counts).reduce((a, b) => a + b, 0), 0);
|
|
407
|
+
const headline = [
|
|
408
|
+
`${decided} of ${c.total} decided`,
|
|
409
|
+
...(c.retest ? [`${c.retest} to retest`] : []),
|
|
410
|
+
].join(" · ");
|
|
411
|
+
if (vocabs.size <= 1) {
|
|
412
|
+
const counts = {};
|
|
413
|
+
for (const l of c.lines)
|
|
414
|
+
for (const [id, n] of Object.entries(l.counts))
|
|
415
|
+
counts[id] = (counts[id] ?? 0) + n;
|
|
416
|
+
return {
|
|
417
|
+
verdicts: c.lines[0]?.words ?? BLOCKS_VOCABULARY,
|
|
418
|
+
counts,
|
|
419
|
+
total: c.total,
|
|
420
|
+
retest: c.retest,
|
|
421
|
+
decidedWord: "decided",
|
|
422
|
+
lines: [],
|
|
423
|
+
headline,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const tones = [
|
|
427
|
+
{ id: "success", label: "success", tone: "success" },
|
|
428
|
+
{ id: "warning", label: "warning", tone: "warning" },
|
|
429
|
+
{ id: "error", label: "error", tone: "error" },
|
|
430
|
+
{ id: "info", label: "info", tone: "info" },
|
|
431
|
+
];
|
|
432
|
+
const byTone = {};
|
|
433
|
+
for (const l of c.lines)
|
|
434
|
+
for (const w of l.words)
|
|
435
|
+
if (l.counts[w.id])
|
|
436
|
+
byTone[w.tone] = (byTone[w.tone] ?? 0) + l.counts[w.id];
|
|
437
|
+
return {
|
|
438
|
+
verdicts: tones,
|
|
439
|
+
counts: byTone,
|
|
440
|
+
total: c.total,
|
|
441
|
+
retest: c.retest,
|
|
442
|
+
decidedWord: "decided",
|
|
443
|
+
lines: c.lines.map((l) => ({
|
|
444
|
+
key: l.key,
|
|
445
|
+
title: l.title,
|
|
446
|
+
verdicts: l.words,
|
|
447
|
+
counts: l.counts,
|
|
448
|
+
total: l.total,
|
|
449
|
+
retest: l.retest,
|
|
450
|
+
decidedWord: "decided",
|
|
451
|
+
})),
|
|
452
|
+
headline,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* The page's still-open review units, in page order — the census walk with
|
|
457
|
+
* IDENTITY, so `openReviewUnits(...).length` is exactly the page's
|
|
458
|
+
* undecided count.
|
|
459
|
+
*/
|
|
460
|
+
export function openReviewUnits(spec, blocks) {
|
|
461
|
+
const out = [];
|
|
462
|
+
const cards = decisionCardsOf(spec);
|
|
463
|
+
for (const b of coerceBlocksSpec(spec)?.blocks ?? []) {
|
|
464
|
+
if (isItemReviewBlock(b.type)) {
|
|
465
|
+
const hook = blockDef(b.type)?.itemReview;
|
|
466
|
+
const context = itemBlockTitle(b);
|
|
467
|
+
for (const it of reviewItemsOf(b.type, b.params, blocks[b.id])) {
|
|
468
|
+
if (it.verdict !== null)
|
|
469
|
+
continue;
|
|
470
|
+
let label = it.id;
|
|
471
|
+
let thumb;
|
|
472
|
+
try {
|
|
473
|
+
label = hook?.itemLabel?.(b.params, it.id) ?? it.id;
|
|
474
|
+
thumb = hook?.itemThumb?.(b.params, it.id);
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// A hook that trips over untrusted JSON keeps the id as the label.
|
|
478
|
+
}
|
|
479
|
+
out.push({
|
|
480
|
+
key: `${b.id}:${it.id}`,
|
|
481
|
+
blockId: b.id,
|
|
482
|
+
type: b.type,
|
|
483
|
+
itemId: it.id,
|
|
484
|
+
label,
|
|
485
|
+
context,
|
|
486
|
+
...(thumb ? { thumb } : {}),
|
|
487
|
+
retest: it.retest,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const card = cards.get(b.id);
|
|
493
|
+
if (!card && !takesBlockVerdict(b.type, b.params))
|
|
494
|
+
continue;
|
|
495
|
+
if (card) {
|
|
496
|
+
const st = blocks[b.id];
|
|
497
|
+
if (isObj(st) && letterForPick(card, st.pick))
|
|
498
|
+
continue;
|
|
499
|
+
out.push({
|
|
500
|
+
key: b.id,
|
|
501
|
+
blockId: b.id,
|
|
502
|
+
type: b.type,
|
|
503
|
+
label: card.question,
|
|
504
|
+
retest: false,
|
|
505
|
+
});
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const rv = blocks[reviewKey(b.id)];
|
|
509
|
+
if (isObj(rv) && (rv.verdict === "approved" || rv.verdict === "needs-pass"))
|
|
510
|
+
continue;
|
|
511
|
+
const p = isObj(b.params) ? b.params : {};
|
|
512
|
+
out.push({
|
|
513
|
+
key: b.id,
|
|
514
|
+
blockId: b.id,
|
|
515
|
+
type: b.type,
|
|
516
|
+
label: typeof p.title === "string" && p.title.trim() ? p.title.trim() : "",
|
|
517
|
+
retest: false,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return out;
|
|
521
|
+
}
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
// Item verdicts, the sent state and the changed-after-send diff
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
/**
|
|
526
|
+
* Item verdicts from item-review blocks in the viewer's saved state, only
|
|
527
|
+
* items the viewer gave a verdict (an authored seed is not theirs). The
|
|
528
|
+
* page's changed-after-send diff runs this SAME read on both sides.
|
|
529
|
+
*/
|
|
530
|
+
export function itemVerdictsOf(saved, spec) {
|
|
531
|
+
const out = {};
|
|
532
|
+
if (!saved)
|
|
533
|
+
return out;
|
|
534
|
+
for (const b of coerceBlocksSpec(spec)?.blocks ?? []) {
|
|
535
|
+
if (!isItemReviewBlock(b.type))
|
|
536
|
+
continue;
|
|
537
|
+
const items = {};
|
|
538
|
+
for (const it of reviewItemsOf(b.type, b.params, saved[b.id])) {
|
|
539
|
+
if (!it.explicit || !it.raw)
|
|
540
|
+
continue;
|
|
541
|
+
const e = { verdict: it.raw };
|
|
542
|
+
if (it.note)
|
|
543
|
+
e.note = it.note;
|
|
544
|
+
if (it.attachments?.length)
|
|
545
|
+
e.attachments = it.attachments;
|
|
546
|
+
items[it.id] = e;
|
|
547
|
+
}
|
|
548
|
+
if (Object.keys(items).length > 0)
|
|
549
|
+
out[b.id] = items;
|
|
550
|
+
}
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
const VERDICT_VALUES = new Set(["approved", "needs-pass", "chose"]);
|
|
554
|
+
function coerceVerdictEntry(v) {
|
|
555
|
+
if (!isObj(v))
|
|
556
|
+
return null;
|
|
557
|
+
if (typeof v.verdict !== "string" || !VERDICT_VALUES.has(v.verdict))
|
|
558
|
+
return null;
|
|
559
|
+
const e = { verdict: v.verdict };
|
|
560
|
+
if (v.option !== undefined) {
|
|
561
|
+
if (typeof v.option !== "string")
|
|
562
|
+
return null;
|
|
563
|
+
e.option = v.option;
|
|
564
|
+
}
|
|
565
|
+
if (v.note !== undefined) {
|
|
566
|
+
if (typeof v.note !== "string")
|
|
567
|
+
return null;
|
|
568
|
+
e.note = v.note;
|
|
569
|
+
}
|
|
570
|
+
if (v.attachments !== undefined) {
|
|
571
|
+
if (!Array.isArray(v.attachments))
|
|
572
|
+
return null;
|
|
573
|
+
e.attachments = v.attachments;
|
|
574
|
+
}
|
|
575
|
+
return e;
|
|
576
|
+
}
|
|
577
|
+
function coerceVerdictsMap(v) {
|
|
578
|
+
if (!isObj(v))
|
|
579
|
+
return undefined;
|
|
580
|
+
const out = {};
|
|
581
|
+
for (const [id, raw] of Object.entries(v)) {
|
|
582
|
+
const e = coerceVerdictEntry(raw);
|
|
583
|
+
if (!e)
|
|
584
|
+
return undefined;
|
|
585
|
+
out[id] = e;
|
|
586
|
+
}
|
|
587
|
+
return out;
|
|
588
|
+
}
|
|
589
|
+
function coerceItemsMap(v) {
|
|
590
|
+
if (!isObj(v))
|
|
591
|
+
return undefined;
|
|
592
|
+
const out = {};
|
|
593
|
+
for (const [blockId, byItem] of Object.entries(v)) {
|
|
594
|
+
if (!isObj(byItem))
|
|
595
|
+
return undefined;
|
|
596
|
+
const items = {};
|
|
597
|
+
for (const [itemId, raw] of Object.entries(byItem)) {
|
|
598
|
+
if (!isObj(raw) || typeof raw.verdict !== "string")
|
|
599
|
+
return undefined;
|
|
600
|
+
items[itemId] = raw;
|
|
601
|
+
}
|
|
602
|
+
out[blockId] = items;
|
|
603
|
+
}
|
|
604
|
+
return out;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* The seeded done state from a stored event payload (`sentVerdicts`,
|
|
608
|
+
* `sentItems`). Untrusted JSON: an entry that does not parse DROPS the
|
|
609
|
+
* whole map — the page then reads "sent, details unknown" — never a wrong
|
|
610
|
+
* changed-state from a malformed row.
|
|
611
|
+
*/
|
|
612
|
+
export function coerceSentAnswer(input) {
|
|
613
|
+
const verdicts = coerceVerdictsMap(input.payload.sentVerdicts);
|
|
614
|
+
const items = coerceItemsMap(input.payload.sentItems);
|
|
615
|
+
return {
|
|
616
|
+
sentAt: input.sentAt,
|
|
617
|
+
sentTo: input.sentTo,
|
|
618
|
+
...(input.eventId ? { eventId: input.eventId } : {}),
|
|
619
|
+
...(input.deliveredAt !== undefined
|
|
620
|
+
? { deliveredAt: input.deliveredAt }
|
|
621
|
+
: {}),
|
|
622
|
+
...(input.deliverable !== undefined
|
|
623
|
+
? { deliverable: input.deliverable }
|
|
624
|
+
: {}),
|
|
625
|
+
...(verdicts ? { verdicts } : {}),
|
|
626
|
+
...(items ? { items } : {}),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
/** Canonical JSON: object keys sorted, so a jsonb round trip can never fake
|
|
630
|
+
* a difference. */
|
|
631
|
+
function canonical(value) {
|
|
632
|
+
if (Array.isArray(value))
|
|
633
|
+
return value.map(canonical);
|
|
634
|
+
if (isObj(value)) {
|
|
635
|
+
const out = {};
|
|
636
|
+
for (const k of Object.keys(value).sort())
|
|
637
|
+
out[k] = canonical(value[k]);
|
|
638
|
+
return out;
|
|
639
|
+
}
|
|
640
|
+
return value;
|
|
641
|
+
}
|
|
642
|
+
const entryJson = (v) => JSON.stringify(canonical(v ?? null));
|
|
643
|
+
/**
|
|
644
|
+
* Which sent entries differ from the viewer's current decisions. Sent maps
|
|
645
|
+
* ABSENT → all zeros: unknown is never reported as changed.
|
|
646
|
+
*/
|
|
647
|
+
export function changedSinceSend(sentVerdicts, currentVerdicts, sentItems, currentItems) {
|
|
648
|
+
if (!sentVerdicts && !sentItems)
|
|
649
|
+
return { blocks: 0, items: 0, total: 0 };
|
|
650
|
+
let blocks = 0;
|
|
651
|
+
const ids = new Set([
|
|
652
|
+
...Object.keys(sentVerdicts ?? {}),
|
|
653
|
+
...Object.keys(currentVerdicts),
|
|
654
|
+
]);
|
|
655
|
+
for (const id of ids)
|
|
656
|
+
if (entryJson(sentVerdicts?.[id]) !== entryJson(currentVerdicts[id]))
|
|
657
|
+
blocks++;
|
|
658
|
+
let items = 0;
|
|
659
|
+
const blockIds = new Set([
|
|
660
|
+
...Object.keys(sentItems ?? {}),
|
|
661
|
+
...Object.keys(currentItems),
|
|
662
|
+
]);
|
|
663
|
+
for (const b of blockIds) {
|
|
664
|
+
const sent = sentItems?.[b] ?? {};
|
|
665
|
+
const cur = currentItems[b] ?? {};
|
|
666
|
+
const itemIds = new Set([...Object.keys(sent), ...Object.keys(cur)]);
|
|
667
|
+
for (const i of itemIds) {
|
|
668
|
+
if (entryJson(sent[i]) !== entryJson(cur[i])) {
|
|
669
|
+
items++;
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return { blocks, items, total: blocks + items };
|
|
675
|
+
}
|
|
676
|
+
/** The one-line "what was sent" of the done state. "" for a note-only answer. */
|
|
677
|
+
export function sentAnswerLine(verdicts, items) {
|
|
678
|
+
let approved = 0;
|
|
679
|
+
let needsPass = 0;
|
|
680
|
+
let chose = 0;
|
|
681
|
+
for (const v of Object.values(verdicts ?? {})) {
|
|
682
|
+
if (v.verdict === "approved")
|
|
683
|
+
approved++;
|
|
684
|
+
else if (v.verdict === "needs-pass")
|
|
685
|
+
needsPass++;
|
|
686
|
+
else if (v.verdict === "chose")
|
|
687
|
+
chose++;
|
|
688
|
+
}
|
|
689
|
+
let itemApproved = 0;
|
|
690
|
+
let itemNeedsWork = 0;
|
|
691
|
+
let itemRejected = 0;
|
|
692
|
+
for (const byItem of Object.values(items ?? {}))
|
|
693
|
+
for (const v of Object.values(byItem)) {
|
|
694
|
+
const t = ITEM_VERDICTS.get(v.verdict.trim().toLowerCase());
|
|
695
|
+
if (t === "approved")
|
|
696
|
+
itemApproved++;
|
|
697
|
+
else if (t === "needs-work")
|
|
698
|
+
itemNeedsWork++;
|
|
699
|
+
else if (t === "rejected")
|
|
700
|
+
itemRejected++;
|
|
701
|
+
}
|
|
702
|
+
const parts = [];
|
|
703
|
+
if (chose)
|
|
704
|
+
parts.push(`${chose} decision${chose === 1 ? "" : "s"} answered`);
|
|
705
|
+
if (approved)
|
|
706
|
+
parts.push(`${approved} approved`);
|
|
707
|
+
if (needsPass)
|
|
708
|
+
parts.push(`${needsPass} need${needsPass === 1 ? "s" : ""} another pass`);
|
|
709
|
+
if (itemApproved)
|
|
710
|
+
parts.push(`${itemApproved} item${itemApproved === 1 ? "" : "s"} approved`);
|
|
711
|
+
if (itemNeedsWork)
|
|
712
|
+
parts.push(`${itemNeedsWork} to redo`);
|
|
713
|
+
if (itemRejected)
|
|
714
|
+
parts.push(`${itemRejected} rejected`);
|
|
715
|
+
return parts.join(" · ");
|
|
716
|
+
}
|