dsh-workflow-map 0.0.0-stage → 0.3.4
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/CHANGELOG.md +184 -0
- package/LICENSE +21 -0
- package/README.en.md +458 -0
- package/README.md +436 -2
- package/client.js +9685 -0
- package/cordis.patch.yml +11 -0
- package/docs/OPEN-SOURCE-GUIDE.md +256 -0
- package/docs/host-compatibility.md +320 -0
- package/docs/screenshot-detail.png +0 -0
- package/docs/screenshot.png +0 -0
- package/icon.svg +6 -0
- package/index.js +12 -0
- package/lib/index.js +974 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +81 -4
- package/src/client/Branches.js +241 -0
- package/src/client/FlowColumn.js +572 -0
- package/src/client/FlowEffects.js +677 -0
- package/src/client/HotInject.js +628 -0
- package/src/client/MainFlow.js +359 -0
- package/src/client/NodeCard.js +545 -0
- package/src/client/NodeDetail.js +569 -0
- package/src/client/ProgressUnder.js +216 -0
- package/src/client/RightPage.js +285 -0
- package/src/client/Settings.js +212 -0
- package/src/client/StepColumn.js +479 -0
- package/src/client/StepDetail.js +250 -0
- package/src/client/export.js +280 -0
- package/src/client/panel.js +1911 -0
- package/src/client/pulse.js +126 -0
- package/src/client/store.js +1090 -0
- package/src/client/theme.js +1176 -0
- package/src/fold.mjs +2763 -0
- package/src/orchestrator.mjs +444 -0
- package/src/route.js +621 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings.js — presentation preferences and the in-panel settings card.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is display-only. A setting never reaches the host half, never
|
|
5
|
+
* changes a snapshot, and never writes anything except the one localStorage record
|
|
6
|
+
* that store.js already owns (`dsh-workflow-map:prefs`).
|
|
7
|
+
*
|
|
8
|
+
* The allow-lists live in theme.js (WF_PALETTES / WF_MOTIONS / WF_DETAIL_MODES /
|
|
9
|
+
* WF_NODE_LIMITS), so the sanitizer, the segmented control and the CSS cannot drift
|
|
10
|
+
* apart. A corrupted / older record degrades field by field to the documented
|
|
11
|
+
* defaults instead of blanking the panel.
|
|
12
|
+
*
|
|
13
|
+
* Pure: `h` and `say` are injected. Inlined into client.js by tools/build-client.mjs.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The documented defaults ("Reset to defaults" restores exactly these values). */
|
|
17
|
+
function wfDefaultSettings() {
|
|
18
|
+
return {
|
|
19
|
+
palette: "rainbow",
|
|
20
|
+
motion: "full",
|
|
21
|
+
detail: "summary",
|
|
22
|
+
nodeLimit: 60,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function wfAllowed(value, allowed, fallback) {
|
|
27
|
+
if (typeof value === "string") {
|
|
28
|
+
for (var i = 0; i < allowed.length; i += 1) if (value === allowed[i]) return value;
|
|
29
|
+
}
|
|
30
|
+
for (var f = 0; f < allowed.length; f += 1) if (fallback === allowed[f]) return fallback;
|
|
31
|
+
return allowed[0];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 60 / 30 / 120 / 0 - 0 means "all". The string "all" is accepted on read so a
|
|
36
|
+
* hand-edited record cannot strand the panel, but it is persisted as 0.
|
|
37
|
+
*/
|
|
38
|
+
function wfSanitizeNodeLimit(value, fallback) {
|
|
39
|
+
var fallbackNumber = wfFinite(fallback) === null ? 60 : Math.round(fallback);
|
|
40
|
+
if (WF_NODE_LIMITS.indexOf(fallbackNumber) === -1) fallbackNumber = 60;
|
|
41
|
+
if (typeof value === "string" && value.toLowerCase() === "all") return 0;
|
|
42
|
+
var number = wfFinite(value);
|
|
43
|
+
if (number === null) return fallbackNumber;
|
|
44
|
+
number = Math.round(number);
|
|
45
|
+
return WF_NODE_LIMITS.indexOf(number) === -1 ? fallbackNumber : number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Sanitize the four presentation fields. `fallback` may be the full prefs record
|
|
50
|
+
* (store.js passes it), so a partial or stale record keeps the fields it still has.
|
|
51
|
+
*/
|
|
52
|
+
function wfSanitizeSettings(input, fallback) {
|
|
53
|
+
var base = fallback && typeof fallback === "object" ? fallback : wfDefaultSettings();
|
|
54
|
+
var defaults = wfDefaultSettings();
|
|
55
|
+
return {
|
|
56
|
+
palette: wfAllowed(input && input.palette, WF_PALETTES, wfAllowed(base.palette, WF_PALETTES, defaults.palette)),
|
|
57
|
+
motion: wfAllowed(input && input.motion, WF_MOTIONS, wfAllowed(base.motion, WF_MOTIONS, defaults.motion)),
|
|
58
|
+
detail: wfAllowed(input && input.detail, WF_DETAIL_MODES, wfAllowed(base.detail, WF_DETAIL_MODES, defaults.detail)),
|
|
59
|
+
nodeLimit: wfSanitizeNodeLimit(input && input.nodeLimit, wfSanitizeNodeLimit(base.nodeLimit, defaults.nodeLimit)),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The compact copy the model carries to the renderers. */
|
|
64
|
+
function wfSettingsOf(prefs) {
|
|
65
|
+
return wfSanitizeSettings(prefs, wfDefaultSettings());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Localised label for one option; values are code, labels are dictionary entries. */
|
|
69
|
+
function wfSettingOptionKey(setting, value) {
|
|
70
|
+
if (setting === "palette") return "panel.settings.palette." + value;
|
|
71
|
+
if (setting === "motion") return "panel.settings.motion." + value;
|
|
72
|
+
if (setting === "detail") return "panel.settings.detail." + value;
|
|
73
|
+
if (setting === "nodeLimit") return value === 0 ? "panel.settings.limit.all" : null;
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function wfSettingOptionLabel(setting, value, say) {
|
|
78
|
+
var key = wfSettingOptionKey(setting, value);
|
|
79
|
+
if (key !== null) return say(key);
|
|
80
|
+
return String(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One segmented row. Buttons are the visible state (`aria-pressed`), not a select. */
|
|
84
|
+
function wfRenderSettingSegment(h, setting, labelKey, values, current, say, actions) {
|
|
85
|
+
var fire = actions || {};
|
|
86
|
+
var buttons = values.map(function (value) {
|
|
87
|
+
var active = value === current;
|
|
88
|
+
var label = wfSettingOptionLabel(setting, value, say);
|
|
89
|
+
return h(
|
|
90
|
+
"button",
|
|
91
|
+
{
|
|
92
|
+
type: "button",
|
|
93
|
+
key: setting + "-" + String(value),
|
|
94
|
+
className: "wf-seg-btn",
|
|
95
|
+
"data-setting": setting,
|
|
96
|
+
"data-value": String(value),
|
|
97
|
+
"data-dsh-workflow-map": "setting-option",
|
|
98
|
+
"aria-pressed": active ? "true" : "false",
|
|
99
|
+
title: say(labelKey) + " · " + label,
|
|
100
|
+
onClick: function () {
|
|
101
|
+
if (typeof fire.onSetting === "function") fire.onSetting(setting, value);
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
label
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
return h(
|
|
108
|
+
"div",
|
|
109
|
+
{ className: "wf-set-grid", key: "row-" + setting, "data-setting-row": setting },
|
|
110
|
+
h("span", { className: "wf-set-label", key: "label" }, say(labelKey)),
|
|
111
|
+
h(
|
|
112
|
+
"div",
|
|
113
|
+
{ className: "wf-seg", key: "seg", role: "group", "aria-label": say(labelKey) },
|
|
114
|
+
buttons
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A copy button: the 1.5 s acknowledgement is repainted by NodeDetail's helper. */
|
|
120
|
+
function wfRenderExportCopy(h, kind, labelKey, say, actions) {
|
|
121
|
+
var fire = actions || {};
|
|
122
|
+
return h(
|
|
123
|
+
"button",
|
|
124
|
+
{
|
|
125
|
+
type: "button",
|
|
126
|
+
key: "copy-" + kind,
|
|
127
|
+
className: "wf-link",
|
|
128
|
+
"data-export": "copy-" + kind,
|
|
129
|
+
"data-wf-copy-label": say(labelKey),
|
|
130
|
+
onClick: function (event) {
|
|
131
|
+
var element = event && event.currentTarget ? event.currentTarget : null;
|
|
132
|
+
if (typeof fire.onExportCopy === "function") fire.onExportCopy(kind, element);
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
h("span", { className: "wf-copy-label", key: "label" }, say(labelKey)),
|
|
136
|
+
h("span", { className: "wf-copy-status", key: "status", "aria-live": "polite" }, "")
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function wfRenderExportDownload(h, kind, labelKey, say, actions) {
|
|
141
|
+
var fire = actions || {};
|
|
142
|
+
return h(
|
|
143
|
+
"button",
|
|
144
|
+
{
|
|
145
|
+
type: "button",
|
|
146
|
+
key: "download-" + kind,
|
|
147
|
+
className: "wf-link",
|
|
148
|
+
"data-export": "download-" + kind,
|
|
149
|
+
title: say(labelKey),
|
|
150
|
+
onClick: function () {
|
|
151
|
+
if (typeof fire.onExportDownload === "function") fire.onExportDownload(kind);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
say(labelKey)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The whole in-panel settings section. It replaces the diagnostic strip:
|
|
160
|
+
* the endpoint / counts / rev remain at the bottom, but the top is now the actual
|
|
161
|
+
* four preferences plus the two export pairs.
|
|
162
|
+
*/
|
|
163
|
+
function wfRenderSettings(h, prefs, say, actions, extras) {
|
|
164
|
+
var fire = actions || {};
|
|
165
|
+
var settings = wfSanitizeSettings(prefs, wfDefaultSettings());
|
|
166
|
+
var ui = extras || {};
|
|
167
|
+
return h(
|
|
168
|
+
"div",
|
|
169
|
+
{ className: "wf-settings", key: "settings", "data-dsh-workflow-map": "settings" },
|
|
170
|
+
wfRenderSettingSegment(h, "palette", "panel.settings.palette", WF_PALETTES, settings.palette, say, fire),
|
|
171
|
+
wfRenderSettingSegment(h, "motion", "panel.settings.motion", WF_MOTIONS, settings.motion, say, fire),
|
|
172
|
+
wfRenderSettingSegment(h, "detail", "panel.settings.detail", WF_DETAIL_MODES, settings.detail, say, fire),
|
|
173
|
+
wfRenderSettingSegment(h, "nodeLimit", "panel.settings.limit", WF_NODE_LIMITS, settings.nodeLimit, say, fire),
|
|
174
|
+
h(
|
|
175
|
+
"div",
|
|
176
|
+
{ className: "wf-set-actions", key: "actions" },
|
|
177
|
+
h(
|
|
178
|
+
"button",
|
|
179
|
+
{
|
|
180
|
+
type: "button",
|
|
181
|
+
key: "reset",
|
|
182
|
+
className: "wf-link",
|
|
183
|
+
"data-action": "reset-prefs",
|
|
184
|
+
title: say("panel.settings.reset.hint"),
|
|
185
|
+
onClick: function () {
|
|
186
|
+
if (typeof fire.onReset === "function") fire.onReset();
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
say("panel.settings.reset")
|
|
190
|
+
),
|
|
191
|
+
h("span", { className: "wf-kv", key: "counts" }, say("panel.settings.counts", {
|
|
192
|
+
steps: ui.steps === undefined ? 0 : ui.steps,
|
|
193
|
+
nodes: ui.nodes === undefined ? 0 : ui.nodes,
|
|
194
|
+
})),
|
|
195
|
+
wfFinite(ui.rev) === null ? null : h("span", { className: "wf-kv", key: "rev" }, say("panel.settings.rev", { rev: ui.rev }))
|
|
196
|
+
),
|
|
197
|
+
h(
|
|
198
|
+
"div",
|
|
199
|
+
{ className: "wf-export", key: "export" },
|
|
200
|
+
h("span", { className: "wf-export-label", key: "label" }, say("panel.settings.export")),
|
|
201
|
+
wfRenderExportCopy(h, "md", "panel.settings.export.markdown", say, fire),
|
|
202
|
+
wfRenderExportCopy(h, "json", "panel.settings.export.json", say, fire),
|
|
203
|
+
wfRenderExportDownload(h, "md", "panel.settings.export.downloadmarkdown", say, fire),
|
|
204
|
+
wfRenderExportDownload(h, "json", "panel.settings.export.downloadjson", say, fire)
|
|
205
|
+
),
|
|
206
|
+
h(
|
|
207
|
+
"div",
|
|
208
|
+
{ className: "wf-set-actions", key: "diag" },
|
|
209
|
+
h("span", { className: "wf-kv", key: "ep" }, say("panel.settings.endpoint") + " " + String(ui.endpoint || ""))
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StepColumn.js — the left column: the task's step chain, one card per step.
|
|
3
|
+
*
|
|
4
|
+
* Each `<li>` is a CARD plus the progress rail that hangs under its frame
|
|
5
|
+
* (ProgressUnder.js) plus — except on the last card — a 14 px connector line.
|
|
6
|
+
* The rail is deliberately OUTSIDE the card's border box with WF_STEP_GAP of
|
|
7
|
+
* air, so the card still reads as a card and the bar as an annotation of it.
|
|
8
|
+
*
|
|
9
|
+
* STATUS POLICY (the one rule this file exists for):
|
|
10
|
+
* PAE `plan.statuses` > the todo table > `plan.anchorSeqs` inference >
|
|
11
|
+
* the host's event-derived status > "pending".
|
|
12
|
+
*
|
|
13
|
+
* Why the browser re-derives what the host already merged: the host may see a
|
|
14
|
+
* PAE-opened row's event status before PAE rewrites `statuses`, and the plan file
|
|
15
|
+
* is the published truth ("completed" must render as completed). `anchorSeqs`
|
|
16
|
+
* covers the window where a step has been opened but no status/report landed yet:
|
|
17
|
+
* a step with an anchor is never "not started".
|
|
18
|
+
*
|
|
19
|
+
* Pure: no React import, no DOM. `h` and `say` are injected so
|
|
20
|
+
* tools/selftest-panel.mjs can drive the exact tree the frame will mount.
|
|
21
|
+
*
|
|
22
|
+
* Inlined into client.js by tools/build-client.mjs (single-file browser half).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** PAE / todo status vocabulary → this plugin's five card states. A Map, so a
|
|
26
|
+
* crafted "constructor" key cannot resolve through Object.prototype. */
|
|
27
|
+
var WF_STATUS_ALIASES = new Map([
|
|
28
|
+
["completed", "done"],
|
|
29
|
+
["complete", "done"],
|
|
30
|
+
["done", "done"],
|
|
31
|
+
["success", "done"],
|
|
32
|
+
["succeeded", "done"],
|
|
33
|
+
["finished", "done"],
|
|
34
|
+
["in_progress", "running"],
|
|
35
|
+
["in-progress", "running"],
|
|
36
|
+
["running", "running"],
|
|
37
|
+
["executing", "running"],
|
|
38
|
+
["processing", "running"],
|
|
39
|
+
["active", "running"],
|
|
40
|
+
["pending", "pending"],
|
|
41
|
+
["queued", "pending"],
|
|
42
|
+
["todo", "pending"],
|
|
43
|
+
["not_started", "pending"],
|
|
44
|
+
["not-started", "pending"],
|
|
45
|
+
["idle", "pending"],
|
|
46
|
+
["failed", "failed"],
|
|
47
|
+
["failure", "failed"],
|
|
48
|
+
["error", "failed"],
|
|
49
|
+
["errored", "failed"],
|
|
50
|
+
["aborted", "failed"],
|
|
51
|
+
["skipped", "skipped"],
|
|
52
|
+
["skip", "skipped"],
|
|
53
|
+
["cancelled", "skipped"],
|
|
54
|
+
["canceled", "skipped"],
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/** The ring glyph per state: ✓ done, ● running, ✕ failed, ○ not started. */
|
|
58
|
+
var WF_STATUS_GLYPH = {
|
|
59
|
+
done: "✓",
|
|
60
|
+
running: "●",
|
|
61
|
+
failed: "✕",
|
|
62
|
+
pending: "○",
|
|
63
|
+
skipped: "–",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
var WF_STATUS_SAY = {
|
|
67
|
+
done: "panel.status.done",
|
|
68
|
+
running: "panel.status.running",
|
|
69
|
+
failed: "panel.status.failed",
|
|
70
|
+
pending: "panel.status.pending",
|
|
71
|
+
skipped: "panel.status.skipped",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** A status label from any of the three sources → canonical state, or null. */
|
|
75
|
+
function wfCanonicalStatus(label) {
|
|
76
|
+
if (typeof label !== "string") return null;
|
|
77
|
+
var key = label.trim().toLowerCase();
|
|
78
|
+
if (key === "") return null;
|
|
79
|
+
return WF_STATUS_ALIASES.get(key) || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function wfStatusSayKey(status) {
|
|
83
|
+
return WF_STATUS_SAY[status] || WF_STATUS_SAY.pending;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function wfStatusGlyph(status) {
|
|
87
|
+
return WF_STATUS_GLYPH[status] || WF_STATUS_GLYPH.pending;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ------------------------------------------------------------------------- */
|
|
91
|
+
/* matching a step to its todo row */
|
|
92
|
+
/* ------------------------------------------------------------------------- */
|
|
93
|
+
|
|
94
|
+
/** "5. install into the desktop profile..." -> "install into the desktop profile..." (the ordinal is not part
|
|
95
|
+
* of the name). Also strips a trailing run of list punctuation. */
|
|
96
|
+
function wfStripStepOrdinal(text) {
|
|
97
|
+
if (typeof text !== "string") return "";
|
|
98
|
+
return text.replace(/^\s*\d+\s*[.、).::]\s*/, "");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function wfNormalizeStepTitle(text) {
|
|
102
|
+
if (typeof text !== "string") return "";
|
|
103
|
+
return wfStripStepOrdinal(text)
|
|
104
|
+
.replace(/\s+/g, " ")
|
|
105
|
+
.replace(/[,。,.、;;::!?!?]+$/g, "")
|
|
106
|
+
.trim()
|
|
107
|
+
.toLowerCase();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The ordinal a todo row carries ("5. …" → 5), or null. */
|
|
111
|
+
function wfTodoOrdinal(content) {
|
|
112
|
+
if (typeof content !== "string") return null;
|
|
113
|
+
var match = /^\s*(\d+)\s*[.、).::]/.exec(content);
|
|
114
|
+
if (!match) return null;
|
|
115
|
+
var value = Number(match[1]);
|
|
116
|
+
return isFinite(value) ? value : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Index the todo table both ways, because the two ways fail differently:
|
|
121
|
+
* an ordinal can be stale after a plan edit, and a title can be re-worded.
|
|
122
|
+
* First row wins (`Map` insertion order), matching the fold's "first match" rule.
|
|
123
|
+
*/
|
|
124
|
+
function wfBuildTodoIndex(todos) {
|
|
125
|
+
var byOrdinal = new Map();
|
|
126
|
+
var byTitle = new Map();
|
|
127
|
+
if (!Array.isArray(todos)) return { byOrdinal: byOrdinal, byTitle: byTitle };
|
|
128
|
+
for (var i = 0; i < todos.length; i += 1) {
|
|
129
|
+
var todo = todos[i];
|
|
130
|
+
if (!todo || typeof todo !== "object") continue;
|
|
131
|
+
var content = typeof todo.content === "string" ? todo.content : "";
|
|
132
|
+
var status = typeof todo.status === "string" ? todo.status : "";
|
|
133
|
+
if (content === "" || status === "") continue;
|
|
134
|
+
var ordinal = wfTodoOrdinal(content);
|
|
135
|
+
if (ordinal !== null && !byOrdinal.has(ordinal)) byOrdinal.set(ordinal, status);
|
|
136
|
+
var title = wfNormalizeStepTitle(content);
|
|
137
|
+
if (title !== "" && !byTitle.has(title)) byTitle.set(title, status);
|
|
138
|
+
}
|
|
139
|
+
return { byOrdinal: byOrdinal, byTitle: byTitle };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* ------------------------------------------------------------------------- */
|
|
143
|
+
/* status derivation */
|
|
144
|
+
/* ------------------------------------------------------------------------- */
|
|
145
|
+
|
|
146
|
+
function wfHasKey(object, key) {
|
|
147
|
+
return !!object && Object.prototype.hasOwnProperty.call(object, key);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The effective status of one row and WHERE it came from (so the card can say
|
|
152
|
+
* `data-status-source` and the test can prove the priority actually held).
|
|
153
|
+
*
|
|
154
|
+
* @returns {{status:string, source:"pae"|"todo"|"anchor"|"host"|"none"}}
|
|
155
|
+
*/
|
|
156
|
+
function wfDeriveStepStatus(row, plan, todoIndex, currentPlanIndex) {
|
|
157
|
+
var planIndex = row && wfFinite(row.planIndex) !== null ? Math.round(row.planIndex) : null;
|
|
158
|
+
|
|
159
|
+
if (plan && planIndex !== null) {
|
|
160
|
+
// 1. PAE's own statuses are the published truth.
|
|
161
|
+
var label = wfHasKey(plan.statuses, String(planIndex)) ? plan.statuses[String(planIndex)] : null;
|
|
162
|
+
var fromPae = wfCanonicalStatus(label);
|
|
163
|
+
var skipped = Array.isArray(plan.skipped) && plan.skipped.indexOf(planIndex) >= 0;
|
|
164
|
+
if (skipped && (fromPae === null || fromPae === "pending")) return { status: "skipped", source: "pae" };
|
|
165
|
+
if (fromPae !== null) return { status: fromPae, source: "pae" };
|
|
166
|
+
|
|
167
|
+
// 2. The todo table (what the model wrote into its own checklist).
|
|
168
|
+
var fromTodo = null;
|
|
169
|
+
if (todoIndex) {
|
|
170
|
+
if (todoIndex.byOrdinal.has(planIndex)) fromTodo = wfCanonicalStatus(todoIndex.byOrdinal.get(planIndex));
|
|
171
|
+
if (fromTodo === null) {
|
|
172
|
+
var title = wfNormalizeStepTitle(row.title);
|
|
173
|
+
if (title !== "" && todoIndex.byTitle.has(title)) fromTodo = wfCanonicalStatus(todoIndex.byTitle.get(title));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (fromTodo !== null) return { status: fromTodo, source: "todo" };
|
|
177
|
+
|
|
178
|
+
// 3. An anchored step has been opened. The plan's own cursor decides whether
|
|
179
|
+
// it is behind us (done), here (running) or still ahead (pending).
|
|
180
|
+
if (wfHasKey(plan.anchorSeqs, String(planIndex))) {
|
|
181
|
+
if (currentPlanIndex !== null && planIndex < currentPlanIndex) return { status: "done", source: "anchor" };
|
|
182
|
+
if (currentPlanIndex !== null && planIndex === currentPlanIndex) return { status: "running", source: "anchor" };
|
|
183
|
+
if (currentPlanIndex !== null && planIndex > currentPlanIndex) return { status: "pending", source: "anchor" };
|
|
184
|
+
return { status: "running", source: "anchor" };
|
|
185
|
+
}
|
|
186
|
+
} else if (todoIndex) {
|
|
187
|
+
// A row with no plan ordinal (planning-phase work) can still be in the todo
|
|
188
|
+
// table; match it by name only.
|
|
189
|
+
var fallbackTitle = wfNormalizeStepTitle(row && row.title);
|
|
190
|
+
if (fallbackTitle !== "" && todoIndex.byTitle.has(fallbackTitle)) {
|
|
191
|
+
var named = wfCanonicalStatus(todoIndex.byTitle.get(fallbackTitle));
|
|
192
|
+
if (named !== null) return { status: named, source: "todo" };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 4. The host's event-derived status, then the honest floor.
|
|
197
|
+
var fromHost = wfCanonicalStatus(row && row.status);
|
|
198
|
+
if (fromHost !== null) return { status: fromHost, source: "host" };
|
|
199
|
+
return { status: "pending", source: "none" };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Rewrite the model's rows with the derived status. Called by wfBuildModel (store.js)
|
|
204
|
+
* so the header counts, the selection fallback and every card agree on one truth.
|
|
205
|
+
*/
|
|
206
|
+
function wfApplyDerivedStatuses(rows, plan, todos) {
|
|
207
|
+
var todoIndex = wfBuildTodoIndex(todos);
|
|
208
|
+
var current = plan && wfFinite(plan.stepIndex) !== null ? Math.round(plan.stepIndex) : null;
|
|
209
|
+
var out = [];
|
|
210
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
211
|
+
var row = rows[i];
|
|
212
|
+
var derived = wfDeriveStepStatus(row, plan, todoIndex, current);
|
|
213
|
+
var requiresConfirmation = false;
|
|
214
|
+
if (plan && plan.confirmations && wfFinite(row.planIndex) !== null) {
|
|
215
|
+
requiresConfirmation = plan.confirmations[String(Math.round(row.planIndex))] === true;
|
|
216
|
+
}
|
|
217
|
+
out.push({
|
|
218
|
+
index: row.index,
|
|
219
|
+
title: row.title,
|
|
220
|
+
status: derived.status,
|
|
221
|
+
statusKey: wfStatusSayKey(derived.status),
|
|
222
|
+
statusSource: derived.source,
|
|
223
|
+
rawStatus: row.status,
|
|
224
|
+
percent: row.percent,
|
|
225
|
+
percentSource: row.percentSource,
|
|
226
|
+
// carried through the status rewrite so every row keeps the stamp the host
|
|
227
|
+
// shipped, whichever policy decided its status.
|
|
228
|
+
lastEstimateAt: wfFinite(row.lastEstimateAt) === null ? 0 : Math.max(0, Math.round(wfFinite(row.lastEstimateAt))),
|
|
229
|
+
nodeCount: row.nodeCount,
|
|
230
|
+
planIndex: row.planIndex,
|
|
231
|
+
anchorSeq: row.anchorSeq,
|
|
232
|
+
requiresConfirmation: requiresConfirmation,
|
|
233
|
+
// carried through the status rewrite: the rows the host folded for this step (an
|
|
234
|
+
// open approval, an unanswered question). Bounded to the same limit the host applies, so a
|
|
235
|
+
// damaged record cannot flood the badge row.
|
|
236
|
+
pendingConfirm: Array.isArray(row.pendingConfirm) ? row.pendingConfirm.slice(0, 8) : [],
|
|
237
|
+
reportSummary: row.reportSummary,
|
|
238
|
+
// the step detail needs the clock and the report; both are carried here so
|
|
239
|
+
// every row has the same shape whether or not it is the selected one.
|
|
240
|
+
startedAt: wfFinite(row.startedAt),
|
|
241
|
+
endedAt: wfFinite(row.endedAt),
|
|
242
|
+
outcome: typeof row.outcome === "string" ? row.outcome : "",
|
|
243
|
+
report: wfStepReport(plan, row.planIndex),
|
|
244
|
+
active: row.active,
|
|
245
|
+
selected: false,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* ------------------------------------------------------------------------- */
|
|
252
|
+
/* rendering */
|
|
253
|
+
/* ------------------------------------------------------------------------- */
|
|
254
|
+
|
|
255
|
+
function wfPad2(value) {
|
|
256
|
+
var text = String(value);
|
|
257
|
+
return text.length >= 2 ? text : "0" + text;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function wfStepFire(fn, argument) {
|
|
261
|
+
if (typeof fn === "function") fn(argument);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The disclosure glyph. Pure, so a self-test can prove it follows the state. */
|
|
265
|
+
function wfTriGlyph(expanded) {
|
|
266
|
+
return expanded === true ? "▾" : "▸";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The ONE disclosure triangle , shared by a step card and a folded run.
|
|
271
|
+
*
|
|
272
|
+
* It is a real <button>, not a ::before glyph: a pseudo-element is not a hit
|
|
273
|
+
* target, cannot be Tab-reached and cannot carry aria-expanded. It is also a
|
|
274
|
+
* SIBLING of the row it discloses — a button inside a button is invalid HTML and
|
|
275
|
+
* unreachable by keyboard — dropped into the SAME grid cell as that row, so the
|
|
276
|
+
* whole 20 px band down the row's height is clickable and the glyph sits in the
|
|
277
|
+
* band the row reserves with padding-left.
|
|
278
|
+
*
|
|
279
|
+
* `extra` carries what only the caller knows:
|
|
280
|
+
* {label, controls, data, key} the aria-label/title, aria-controls target,
|
|
281
|
+
* the data-dsh-workflow-map tag and the React key.
|
|
282
|
+
*/
|
|
283
|
+
function wfTri(h, expanded, onToggle, extra) {
|
|
284
|
+
var open = expanded === true;
|
|
285
|
+
var fire = typeof onToggle === "function" ? onToggle : null;
|
|
286
|
+
var info = extra && typeof extra === "object" ? extra : {};
|
|
287
|
+
var label = typeof info.label === "string" ? info.label : "";
|
|
288
|
+
return h(
|
|
289
|
+
"button",
|
|
290
|
+
{
|
|
291
|
+
type: "button",
|
|
292
|
+
className: "wf-tri",
|
|
293
|
+
key: typeof info.key === "string" ? info.key : "tri",
|
|
294
|
+
"data-dsh-workflow-map": typeof info.data === "string" ? info.data : "tri",
|
|
295
|
+
"data-expanded": open ? "true" : "false",
|
|
296
|
+
"aria-expanded": open ? "true" : "false",
|
|
297
|
+
"aria-controls": typeof info.controls === "string" ? info.controls : undefined,
|
|
298
|
+
title: label,
|
|
299
|
+
"aria-label": label,
|
|
300
|
+
onClick: function () {
|
|
301
|
+
if (fire !== null) fire();
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
wfTriGlyph(open)
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* One `<li>`: card (+ its 「i」) + rail + the step detail + (unless last) the
|
|
310
|
+
* 14 px connector to the next card. `info` carries {detail, open} for THIS row only.
|
|
311
|
+
*/
|
|
312
|
+
function wfRenderStepItem(h, row, position, total, say, actions, info) {
|
|
313
|
+
var status = row.status;
|
|
314
|
+
var fire = actions || {};
|
|
315
|
+
var glyph = wfStatusGlyph(status);
|
|
316
|
+
var detail = info && info.detail && info.detail.index === row.index ? info.detail : null;
|
|
317
|
+
// `asOf` is the ONE moment every timing segment is measured against. The host
|
|
318
|
+
// ships it in the view; passing it down keeps the renderer clock-free and the numbers
|
|
319
|
+
// reproducible in a self-test with a fixed stamp.
|
|
320
|
+
var compact = !!(info && info.compact === true);
|
|
321
|
+
var open = detail !== null && info.open === true && !compact;
|
|
322
|
+
|
|
323
|
+
var card = h(
|
|
324
|
+
"button",
|
|
325
|
+
{
|
|
326
|
+
type: "button",
|
|
327
|
+
className: "wf-step-card",
|
|
328
|
+
key: "card",
|
|
329
|
+
"data-dsh-workflow-map": "step",
|
|
330
|
+
"data-step-index": row.index,
|
|
331
|
+
"data-plan-index": row.planIndex === null ? undefined : row.planIndex,
|
|
332
|
+
"data-status": status,
|
|
333
|
+
"data-status-source": row.statusSource,
|
|
334
|
+
"data-selected": row.selected ? "true" : "false",
|
|
335
|
+
"data-active": row.active ? "true" : "false",
|
|
336
|
+
"aria-pressed": row.selected ? "true" : "false",
|
|
337
|
+
title: row.title,
|
|
338
|
+
onClick: function () {
|
|
339
|
+
wfStepFire(fire.onSelectStep, row.index);
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
h("span", { className: "wf-step-ring", key: "ring", "aria-hidden": "true" }, glyph),
|
|
343
|
+
h(
|
|
344
|
+
"span",
|
|
345
|
+
{ className: "wf-step-main", key: "main" },
|
|
346
|
+
h("span", { className: "wf-step-title", key: "title" }, row.title),
|
|
347
|
+
row.reportSummary === "" || compact
|
|
348
|
+
? null
|
|
349
|
+
: h("span", { className: "wf-step-sum", key: "sum", title: row.reportSummary }, row.reportSummary),
|
|
350
|
+
h(
|
|
351
|
+
"span",
|
|
352
|
+
{ className: "wf-step-meta", key: "meta" },
|
|
353
|
+
h("span", { className: "wf-badge", key: "badge" }, say(wfStatusSayKey(status))),
|
|
354
|
+
row.requiresConfirmation
|
|
355
|
+
? h(
|
|
356
|
+
"span",
|
|
357
|
+
{ className: "wf-step-flag", key: "flag", title: say("panel.step.confirm") },
|
|
358
|
+
say("panel.step.confirm")
|
|
359
|
+
)
|
|
360
|
+
: null
|
|
361
|
+
),
|
|
362
|
+
// "working ..." only on the step that is running right now: a 6 px breathing dot
|
|
363
|
+
// plus a three-dot ellipsis. It sits under the title line, so a running card never
|
|
364
|
+
// reflows the two-line clamp of `.wf-step-title`.
|
|
365
|
+
status === "running" ? wfRenderLive(h, say, "step") : null
|
|
366
|
+
),
|
|
367
|
+
h("span", { className: "wf-step-seq", key: "seq" }, wfPad2(position + 1) + "/" + wfPad2(total))
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
return h(
|
|
371
|
+
"li",
|
|
372
|
+
{
|
|
373
|
+
className: "wf-step-item",
|
|
374
|
+
key: "step-" + row.index,
|
|
375
|
+
"data-step-index": row.index,
|
|
376
|
+
"data-status": status,
|
|
377
|
+
"data-selected": row.selected ? "true" : "false",
|
|
378
|
+
"data-detail": compact ? "title" : "summary",
|
|
379
|
+
},
|
|
380
|
+
// the disclosure triangle is a real <button>, a SIBLING of the card (a
|
|
381
|
+
// button inside a button is invalid HTML and unreachable by keyboard). The <li> is a
|
|
382
|
+
// one-column grid, so the triangle and the card share cell 1/1: the triangle spans the
|
|
383
|
+
// card's full height in the 20 px band the card reserves with padding-left, and the
|
|
384
|
+
// card stays a DIRECT child (the running-frame ring and FlowEffects query it that way).
|
|
385
|
+
compact
|
|
386
|
+
? null
|
|
387
|
+
: wfTri(h, open, function () {
|
|
388
|
+
wfStepFire(fire.onStepInfo, row.index);
|
|
389
|
+
}, {
|
|
390
|
+
key: "tri",
|
|
391
|
+
data: "step-tri",
|
|
392
|
+
label: say(open ? "panel.step.collapse" : "panel.step.expand"),
|
|
393
|
+
controls: "wf-step-detail-" + row.index,
|
|
394
|
+
}),
|
|
395
|
+
card,
|
|
396
|
+
// The 「i」 is a SIBLING of the card too (same reason as the triangle): it is absolutely
|
|
397
|
+
// positioned inside the 26 px right band the card reserves, so the title never runs
|
|
398
|
+
// underneath it. keeps it — two entry points, one state source.
|
|
399
|
+
compact ? null : h(
|
|
400
|
+
"button",
|
|
401
|
+
{
|
|
402
|
+
type: "button",
|
|
403
|
+
className: "wf-step-info",
|
|
404
|
+
key: "info",
|
|
405
|
+
"data-dsh-workflow-map": "step-info",
|
|
406
|
+
"data-step-index": row.index,
|
|
407
|
+
"data-open": open ? "true" : "false",
|
|
408
|
+
"aria-expanded": open ? "true" : "false",
|
|
409
|
+
"aria-controls": "wf-step-detail-" + row.index,
|
|
410
|
+
title: say(open ? "panel.step.detailClose" : "panel.step.detailOpen"),
|
|
411
|
+
"aria-label": say(open ? "panel.step.detailClose" : "panel.step.detailOpen"),
|
|
412
|
+
onClick: function () {
|
|
413
|
+
wfStepFire(fire.onStepInfo, row.index);
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
"i"
|
|
417
|
+
),
|
|
418
|
+
// the rail carries the number, its age and its projection. `compact` is passed on
|
|
419
|
+
// the row because the dock renders the two-line layout only when there is room for it.
|
|
420
|
+
wfRenderProgressDock(h, compact ? { ...row, compact: true } : row, say, info && info.asOf),
|
|
421
|
+
open ? wfRenderStepDetail(h, detail, say, actions) : null,
|
|
422
|
+
position + 1 < total
|
|
423
|
+
? h("span", {
|
|
424
|
+
className: "wf-step-link",
|
|
425
|
+
key: "link",
|
|
426
|
+
"aria-hidden": "true",
|
|
427
|
+
"data-done": status === "done" ? "true" : "false",
|
|
428
|
+
})
|
|
429
|
+
: null
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* The whole left column: a sticky header inside an internally scrolling list, so
|
|
435
|
+
* a 15-step plan keeps its "total steps · 5/15" caption while the cards move.
|
|
436
|
+
*/
|
|
437
|
+
function wfRenderStepColumn(h, model, say, actions, ui) {
|
|
438
|
+
var total = model.steps.length;
|
|
439
|
+
// `ui` is the closed step index (number) or null; the detail itself is built by store.js
|
|
440
|
+
// for the selected step only, so a stale index can never render another step's report.
|
|
441
|
+
var detail = model.detail && model.detail.index === model.selected ? model.detail : null;
|
|
442
|
+
var header = h(
|
|
443
|
+
"div",
|
|
444
|
+
{ className: "wf-col-h wf-col-h-sticky", key: "h" },
|
|
445
|
+
h("span", { key: "label" }, say("panel.steps")),
|
|
446
|
+
h("span", { className: "wf-count", key: "count" }, model.progress.done + "/" + model.progress.total),
|
|
447
|
+
wfRenderStepHead(h, detail, say)
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
return h(
|
|
451
|
+
"div",
|
|
452
|
+
{
|
|
453
|
+
className: "wf-col wf-col-steps",
|
|
454
|
+
key: "left",
|
|
455
|
+
"data-dsh-workflow-map": "steps",
|
|
456
|
+
"data-many": total > WF_STEP_MANY ? "true" : "false",
|
|
457
|
+
},
|
|
458
|
+
h(
|
|
459
|
+
"div",
|
|
460
|
+
{ className: "wf-scroll wf-steps-scroll", key: "list" },
|
|
461
|
+
header,
|
|
462
|
+
h(
|
|
463
|
+
"ul",
|
|
464
|
+
{ className: "wf-steps", key: "steps", role: "list" },
|
|
465
|
+
model.steps.map(function (row, position) {
|
|
466
|
+
return wfRenderStepItem(h, row, position, total, say, actions, {
|
|
467
|
+
detail: detail,
|
|
468
|
+
open: detail !== null && !(model.settings && model.settings.detail === "title") && wfStepDetailOpen(ui, model.selected),
|
|
469
|
+
compact: !!(model.settings && model.settings.detail === "title"),
|
|
470
|
+
// the shared "now" for every rail's age arithmetic. `view.asOf` is the
|
|
471
|
+
// host's own stamp (the ONE honest clock in the plugin); `model.now` is the render
|
|
472
|
+
// moment the panel passed in and is only a fallback for a view that carries none.
|
|
473
|
+
asOf: model.asOf === null || model.asOf === undefined ? model.now : model.asOf,
|
|
474
|
+
});
|
|
475
|
+
})
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
);
|
|
479
|
+
}
|