dsh-workflow-map 0.0.0-stage → 0.3.2
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 +98 -0
- package/LICENSE +21 -0
- package/README.en.md +447 -0
- package/README.md +423 -2
- package/client.js +9673 -0
- package/cordis.patch.yml +11 -0
- package/docs/OPEN-SOURCE-GUIDE.md +256 -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 +65 -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 +1164 -0
- package/src/fold.mjs +2633 -0
- package/src/orchestrator.mjs +426 -0
- package/src/route.js +621 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressUnder.js — the rail that hangs UNDER a step card's frame.
|
|
3
|
+
*
|
|
4
|
+
* Contract :
|
|
5
|
+
* - exactly WF_STEP_GAP (8 px) of air between the card's bottom border and the
|
|
6
|
+
* rail: the rail belongs to the card but must not be glued to its frame;
|
|
7
|
+
* - a WF_STEP_RAIL (6 px) track whose fill is coloured by state — running
|
|
8
|
+
* flows, done is solid, failed is red, not started leaves the track empty;
|
|
9
|
+
* - the number on the right says WHICH KIND of number it is:
|
|
10
|
+
* "~62% estimated" a time-baseline extrapolation (percentSource "baseline", and its cold-start
|
|
11
|
+
* sub-variant "baseline:steps", which keeps the same visible flavour)
|
|
12
|
+
* "62% self-reported" the model's own report (percentSource "model")
|
|
13
|
+
* "62% measured" the todo table's measured completion ratio (percentSource "todos")
|
|
14
|
+
* "100%" a closed step's pinned value
|
|
15
|
+
* - a step that has NOT started never shows a percentage, even when the wire
|
|
16
|
+
* payload carries a stale one. No number is better than a fake number.
|
|
17
|
+
*
|
|
18
|
+
* adds a SECOND line under the rail — one secondary fact about the
|
|
19
|
+
* number, never about the work:
|
|
20
|
+
* - `ago`: how long ago that number was estimated (`view.asOf − step.lastEstimateAt`).
|
|
21
|
+
* Data freshness: a 40-minute-old estimate on a still-running step is not a
|
|
22
|
+
* lie, but the user deserves to know it is stale. `lastEstimateAt` is 0 when
|
|
23
|
+
* no estimate ever arrived, and then the segment is NOT rendered at all —
|
|
24
|
+
* "0 seconds ago" would be a fabricated number, which is exactly what this plugin
|
|
25
|
+
* refuses to print.
|
|
26
|
+
*
|
|
27
|
+
* (2026-10-26) · the projected REMAINING half is GONE by user request. It used to be shipped
|
|
28
|
+
* by the host as a derived millisecond field (median of the finished steps, hard-bounded) and
|
|
29
|
+
* rendered as "remaining ~N minutes"; the host no longer computes it and this rail no longer renders it.
|
|
30
|
+
* One line, one fact, no consumer-less field — and a payload that still carries that field (an
|
|
31
|
+
* older host) is simply not read.
|
|
32
|
+
*
|
|
33
|
+
* Layout : two right-aligned lines. Line 1 = track + percent (unchanged).
|
|
34
|
+
* Line 2 = ago, one font step and one colour step down, so it reads as
|
|
35
|
+
* secondary. With no age to show the whole line is omitted — never a placeholder.
|
|
36
|
+
* In the narrow container (`data-detail='title'`) only line 1 exists — the host
|
|
37
|
+
* hides line 2 with CSS, and the compact renderer never emits it.
|
|
38
|
+
*
|
|
39
|
+
* Pure: no React import, no DOM, no storage — the same functions run in Node for
|
|
40
|
+
* tools/selftest-panel.mjs and in the browser. `h` (React.createElement) and
|
|
41
|
+
* `say` are injected, so this chunk can be asserted without React.
|
|
42
|
+
*
|
|
43
|
+
* Inlined into client.js by tools/build-client.mjs (single-file browser half).
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** The rail's three label flavours, as opposed to "no label at all". */
|
|
47
|
+
function wfProgressLabel(percent, source, status, say) {
|
|
48
|
+
var value = wfFinite(percent);
|
|
49
|
+
if (value === null) return null;
|
|
50
|
+
// A step that has not started has no progress to report. Showing a number here
|
|
51
|
+
// would be the exact lie the acceptance criterion forbids.
|
|
52
|
+
if (status === "pending" || status === "skipped") return null;
|
|
53
|
+
var rounded = Math.round(wfClamp(value, 0, 100));
|
|
54
|
+
// the cold-start sub-variant of the baseline keeps the SAME visible flavour (~N% estimated):
|
|
55
|
+
// the sub-division is machine-readable (`baseline:steps`), and falling through to the closed
|
|
56
|
+
// flavour would print it as a pinned 100 %-style number, which it is not.
|
|
57
|
+
if (source === "baseline" || source === "baseline:steps") {
|
|
58
|
+
return { kind: "estimate", percent: rounded, text: say("panel.percent.estimate", { percent: rounded }) };
|
|
59
|
+
}
|
|
60
|
+
if (source === "model") {
|
|
61
|
+
return { kind: "self", percent: rounded, text: say("panel.percent.self", { percent: rounded }) };
|
|
62
|
+
}
|
|
63
|
+
// a MEASURED number (the todo table's own completion ratio). It must read differently
|
|
64
|
+
// from both "self-reported" and "estimated", or the user cannot tell a fact from a guess.
|
|
65
|
+
if (source === "todos") {
|
|
66
|
+
return { kind: "real", percent: rounded, text: say("panel.percent.real", { percent: rounded }) };
|
|
67
|
+
}
|
|
68
|
+
return { kind: "closed", percent: rounded, text: rounded + "%" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Fill width in percent of the track. A running step with no number yet gets a
|
|
73
|
+
* narrow indeterminate segment (animated, no digits) instead of a fake value.
|
|
74
|
+
*/
|
|
75
|
+
function wfProgressFillWidth(row) {
|
|
76
|
+
var status = row && typeof row.status === "string" ? row.status : "pending";
|
|
77
|
+
if (status === "pending" || status === "skipped") return 0;
|
|
78
|
+
var value = wfFinite(row.percent);
|
|
79
|
+
if (value === null) return status === "running" ? 30 : 0;
|
|
80
|
+
return Math.round(wfClamp(value, 0, 100));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function wfProgressIndeterminate(row) {
|
|
84
|
+
var status = row && typeof row.status === "string" ? row.status : "pending";
|
|
85
|
+
return status === "running" && wfFinite(row.percent) === null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 180_000 -> "3 minutes", 5_400 -> "54 seconds", 3_720_000 -> "62 minutes".
|
|
90
|
+
* One decimal below ten seconds, integers above — a clock's precision, not a
|
|
91
|
+
* stopwatch's. 0 or less returns "" (never "0 seconds": see the caller).
|
|
92
|
+
*/
|
|
93
|
+
function wfProgressDurationText(ms) {
|
|
94
|
+
var value = wfFinite(ms);
|
|
95
|
+
if (value === null || value <= 0) return "";
|
|
96
|
+
if (value < 10000) {
|
|
97
|
+
var seconds = Math.round(value / 100) / 10;
|
|
98
|
+
return (seconds % 1 === 0 ? String(Math.round(seconds)) : seconds.toFixed(1)) + " 秒";
|
|
99
|
+
}
|
|
100
|
+
if (value < 60000) return Math.round(value / 1000) + " 秒";
|
|
101
|
+
if (value < 3600000) {
|
|
102
|
+
var minutes = Math.floor(value / 60000);
|
|
103
|
+
return minutes + " 分钟";
|
|
104
|
+
}
|
|
105
|
+
return Math.floor(value / 3600000) + " 小时";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* the ONE secondary fact left — how stale the number being quoted is — or null. Pure
|
|
110
|
+
* arithmetic on the row the host already shipped; no clock is read here (the caller passes `asOf`).
|
|
111
|
+
*
|
|
112
|
+
* `ago` needs a real `lastEstimateAt` AND an `asOf` after it: a step that has never carried a
|
|
113
|
+
* number gets no segment, and a future stamp (a re-ordered event log) is clamped to nothing
|
|
114
|
+
* rather than shown as a negative duration. A missing field produces NO segment — never a
|
|
115
|
+
* fabricated 0 or "—" (the ETA half went with the host's derived field).
|
|
116
|
+
*/
|
|
117
|
+
function wfProgressTiming(row, asOf) {
|
|
118
|
+
var status = row && typeof row.status === "string" ? row.status : "pending";
|
|
119
|
+
var now = wfFinite(asOf);
|
|
120
|
+
var estimateAt = wfFinite(row ? row.lastEstimateAt : null);
|
|
121
|
+
// The gate mirrors the percent label's: only a step that HAS a live progress number can read
|
|
122
|
+
// "estimated N minutes ago", because the sentence is about that number. A pending row with a leftover stamp
|
|
123
|
+
// (a re-plan that reset a step, say) would otherwise advertise an estimate it never made — the
|
|
124
|
+
// same "no number beats a fake number" rule the percent label already follows. `ago` is a fact
|
|
125
|
+
// about the PAST, so it only needs a number to have existed at all: percent > 0.
|
|
126
|
+
var livePercent = wfFinite(row ? row.percent : null);
|
|
127
|
+
var hasAnyNumber = livePercent !== null && livePercent > 0 && status !== "pending";
|
|
128
|
+
var ago = null;
|
|
129
|
+
if (hasAnyNumber && now !== null && estimateAt !== null && estimateAt > 0 && now > estimateAt) {
|
|
130
|
+
ago = wfProgressDurationText(now - estimateAt);
|
|
131
|
+
if (ago === "") ago = null;
|
|
132
|
+
}
|
|
133
|
+
return { ago: ago };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** One rail: track + fill + the right-hand number (omitted when there is none). */
|
|
137
|
+
function wfRenderProgressDock(h, row, say, asOf) {
|
|
138
|
+
var status = row && typeof row.status === "string" ? row.status : "pending";
|
|
139
|
+
var label = wfProgressLabel(row ? row.percent : null, row ? row.percentSource : "none", status, say);
|
|
140
|
+
var width = wfProgressFillWidth(row);
|
|
141
|
+
var indeterminate = wfProgressIndeterminate(row);
|
|
142
|
+
var compact = !!(row && row.compact === true);
|
|
143
|
+
var ago = compact ? null : wfProgressTiming(row, asOf).ago;
|
|
144
|
+
|
|
145
|
+
var trackProps = {
|
|
146
|
+
className: "wf-prog-track",
|
|
147
|
+
key: "track",
|
|
148
|
+
role: "progressbar",
|
|
149
|
+
"aria-label": say("panel.step.progress", { title: row && row.title ? row.title : "" }),
|
|
150
|
+
"aria-valuemin": 0,
|
|
151
|
+
"aria-valuemax": 100,
|
|
152
|
+
};
|
|
153
|
+
if (label !== null) {
|
|
154
|
+
trackProps["aria-valuenow"] = label.percent;
|
|
155
|
+
trackProps["aria-valuetext"] = label.text;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// the second line — ONE fact (the age of the number), so there is no bits array and
|
|
159
|
+
// no separator any more. the projected remainder is gone, so `data-timing` converged to
|
|
160
|
+
// none|one; nothing at all renders when there is no age (never a placeholder).
|
|
161
|
+
var subline = ago === null
|
|
162
|
+
? null
|
|
163
|
+
: h(
|
|
164
|
+
"div",
|
|
165
|
+
{ className: "wf-prog-sub", key: "sub" },
|
|
166
|
+
h(
|
|
167
|
+
"span",
|
|
168
|
+
{
|
|
169
|
+
className: "wf-prog-ago",
|
|
170
|
+
key: "ago",
|
|
171
|
+
title: say("panel.percent.ago", { duration: ago }),
|
|
172
|
+
},
|
|
173
|
+
say("panel.percent.ago", { duration: ago })
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return h(
|
|
178
|
+
"div",
|
|
179
|
+
{
|
|
180
|
+
className: "wf-prog-dock",
|
|
181
|
+
key: "dock",
|
|
182
|
+
"data-dsh-workflow-map": "progress",
|
|
183
|
+
"data-step-index": row ? row.index : undefined,
|
|
184
|
+
"data-status": status,
|
|
185
|
+
"data-indeterminate": indeterminate ? "true" : "false",
|
|
186
|
+
"data-timing": ago === null ? "none" : "one",
|
|
187
|
+
},
|
|
188
|
+
h(
|
|
189
|
+
"div",
|
|
190
|
+
{ className: "wf-prog-row", key: "row" },
|
|
191
|
+
h("div", trackProps, h("div", {
|
|
192
|
+
className: "wf-prog-fill",
|
|
193
|
+
key: "fill",
|
|
194
|
+
"data-indeterminate": indeterminate ? "true" : "false",
|
|
195
|
+
// Exactly one step's rail carries the flowing gradient: decided by the caller's row.live (the host marks only
|
|
196
|
+
// the single running row), not "everything running animates". Without that field it falls back to the old behaviour.
|
|
197
|
+
"data-live": (row && row.live === true) || (row && row.live === undefined && status === "running") ? "true" : "false",
|
|
198
|
+
style: { width: width + "%" },
|
|
199
|
+
})),
|
|
200
|
+
label === null
|
|
201
|
+
? null
|
|
202
|
+
: h(
|
|
203
|
+
"span",
|
|
204
|
+
{
|
|
205
|
+
className: "wf-prog-label",
|
|
206
|
+
key: "label",
|
|
207
|
+
"data-kind": label.kind,
|
|
208
|
+
"data-percent": label.percent,
|
|
209
|
+
title: label.text,
|
|
210
|
+
},
|
|
211
|
+
label.text
|
|
212
|
+
)
|
|
213
|
+
),
|
|
214
|
+
subline
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RightPage.js — the RIGHT-HAND page, opened by a click on the main flow or on a circle.
|
|
3
|
+
*
|
|
4
|
+
* One page, three kinds of content, decided by the click that opened it:
|
|
5
|
+
*
|
|
6
|
+
* kind "step" → that step's full record. The report / artifacts / statistics block is the
|
|
7
|
+
* EXISTING renderer (wfRenderStepDetail); what this file adds is the
|
|
8
|
+
* per-type list above it — "this step touched .md ×3, pwsh ×2 …" — each row a
|
|
9
|
+
* button that switches the page to that type. That is how a main-flow click can
|
|
10
|
+
* walk into a type without going back to the circle.
|
|
11
|
+
* kind "type" → every call of ONE file type (a step's circle, or the whole session when the
|
|
12
|
+
* click came from the header's type strip). Each call expands IN PLACE into
|
|
13
|
+
* 's node detail, so the page never grows a second detail vocabulary.
|
|
14
|
+
* kind "inject"→ the hot-inject composer (HotInject.js), mounted here instead of a third column.
|
|
15
|
+
*
|
|
16
|
+
* Nothing is invented: a type that no call supports renders the empty hint, and a step whose
|
|
17
|
+
* nodes the host never sent says so (the count comes from the host, the list from the wire).
|
|
18
|
+
*
|
|
19
|
+
* Pure: `h` and `say` are injected; no React import, no DOM.
|
|
20
|
+
*
|
|
21
|
+
* Inlined into client.js by tools/build-client.mjs (single-file browser half).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The title line: step N · title / .md · 3 calls / hot injection. */
|
|
25
|
+
function wfPageTitle(kind, say, ctx) {
|
|
26
|
+
var context = ctx || {};
|
|
27
|
+
if (kind === "inject") return say("panel.page.inject");
|
|
28
|
+
if (kind === "type") {
|
|
29
|
+
return say(context.scopeStep === null ? "panel.page.typeSession" : "panel.page.typeStep", {
|
|
30
|
+
label: context.label || "—",
|
|
31
|
+
n: wfFinite(context.scopeStep) === null ? "" : context.scopeStep + 1,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return say("panel.page.step", { n: (wfFinite(context.stepIndex) === null ? 0 : context.stepIndex) + 1 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The close button. One per page; Escape does the same thing (wfBindEscape → onEscapeDetail). */
|
|
38
|
+
function wfPageClose(h, say, actions) {
|
|
39
|
+
var fire = actions || {};
|
|
40
|
+
return h(
|
|
41
|
+
"button",
|
|
42
|
+
{
|
|
43
|
+
type: "button",
|
|
44
|
+
className: "wf-page-close",
|
|
45
|
+
key: "close",
|
|
46
|
+
"data-dsh-workflow-map": "page-close",
|
|
47
|
+
title: say("panel.page.close"),
|
|
48
|
+
"aria-label": say("panel.page.close"),
|
|
49
|
+
onClick: function () {
|
|
50
|
+
if (typeof fire.onClosePage === "function") fire.onClosePage();
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
"×"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A call row of the type page. It is a <button> that opens 's node detail under itself —
|
|
59
|
+
* the SAME wfRenderNodeDetail the flow column uses, fed the SAME node, so the privacy rule and the
|
|
60
|
+
* 200-character caps are whatever already guarantees.
|
|
61
|
+
*/
|
|
62
|
+
function wfRenderCallRow(h, node, say, actions, ctx) {
|
|
63
|
+
var context = ctx || {};
|
|
64
|
+
var fire = actions || {};
|
|
65
|
+
var item = node && typeof node === "object" ? node : {};
|
|
66
|
+
var id = typeof item.id === "string" ? item.id : "";
|
|
67
|
+
var open = context.openCall === id && id !== "";
|
|
68
|
+
var status = wfNodeStatus(item, context.activeNodeId);
|
|
69
|
+
var parts = wfNodeTitleParts(item, wfNodeCategory(item), say);
|
|
70
|
+
|
|
71
|
+
var label = [];
|
|
72
|
+
if (parts.verb !== "") label.push(parts.verb, " · ");
|
|
73
|
+
label.push(parts.target);
|
|
74
|
+
|
|
75
|
+
return h(
|
|
76
|
+
"li",
|
|
77
|
+
{ className: "wf-call", key: "call-" + id, "data-status": status, "data-node-id": id },
|
|
78
|
+
h(
|
|
79
|
+
"button",
|
|
80
|
+
{
|
|
81
|
+
type: "button",
|
|
82
|
+
className: "wf-call-head",
|
|
83
|
+
key: "head",
|
|
84
|
+
"data-dsh-workflow-map": "call",
|
|
85
|
+
"data-node-id": id,
|
|
86
|
+
"data-open": open ? "true" : "false",
|
|
87
|
+
"aria-expanded": open ? "true" : "false",
|
|
88
|
+
title: parts.verb !== "" ? parts.verb + " · " + parts.target : parts.target,
|
|
89
|
+
onClick: function () {
|
|
90
|
+
if (typeof fire.onSelectCall === "function") fire.onSelectCall(id);
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
h("span", { className: "wf-call-main", key: "main" }, label),
|
|
94
|
+
h(
|
|
95
|
+
"span",
|
|
96
|
+
{ className: "wf-call-meta", key: "meta" },
|
|
97
|
+
wfFormatClock(item.at) === "" ? "" : wfFormatClock(item.at),
|
|
98
|
+
wfFormatDuration(item.durationMs) === "" ? "" : " · " + wfFormatDuration(item.durationMs)
|
|
99
|
+
),
|
|
100
|
+
h("span", {
|
|
101
|
+
className: "wf-flow-status",
|
|
102
|
+
key: "status",
|
|
103
|
+
"data-status": status,
|
|
104
|
+
"aria-hidden": "true",
|
|
105
|
+
})
|
|
106
|
+
),
|
|
107
|
+
open ? wfRenderNodeDetail(h, item, say, fire, { detail: "summary" }) : null
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The type page's body: the calls of one bucket, in wire order, plus the honest note when the
|
|
113
|
+
* host never sent some of them (the bucket carries the host's count, the list only what arrived).
|
|
114
|
+
*/
|
|
115
|
+
function wfRenderTypeBody(h, bucket, nodes, say, actions, ctx) {
|
|
116
|
+
var context = ctx || {};
|
|
117
|
+
var list = Array.isArray(nodes) ? nodes : [];
|
|
118
|
+
var total = wfFinite(bucket && bucket.count) === null ? list.length : Math.max(0, Math.round(bucket.count));
|
|
119
|
+
|
|
120
|
+
if (list.length === 0) {
|
|
121
|
+
return h("div", { className: "wf-empty", key: "empty" }, say("panel.page.typeEmpty"));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
var more = total > list.length ? say("panel.page.truncated", { count: total - list.length }) : "";
|
|
125
|
+
|
|
126
|
+
return h(
|
|
127
|
+
"div",
|
|
128
|
+
{ className: "wf-calls", key: "calls", "data-dsh-workflow-map": "calls", "data-count": list.length },
|
|
129
|
+
h(
|
|
130
|
+
"ul",
|
|
131
|
+
{ className: "wf-call-list", key: "list", role: "list" },
|
|
132
|
+
list.map(function (node) {
|
|
133
|
+
return wfRenderCallRow(h, node, say, actions, context);
|
|
134
|
+
})
|
|
135
|
+
),
|
|
136
|
+
more === "" ? null : h("div", { className: "wf-detail-note", key: "more" }, more)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The step page's body: the type summary, then 's own detail block.
|
|
142
|
+
*
|
|
143
|
+
* The type summary is the ONLY new thing, and it exists because the page is reached by clicking a
|
|
144
|
+
* unit, not a circle: without it the circles would be the only way into a type. Each row is a
|
|
145
|
+
* button with the same `onOpenType(stepIndex, key)` action the circles fire.
|
|
146
|
+
*/
|
|
147
|
+
function wfRenderStepTypeSummary(h, unit, say, actions, ctx) {
|
|
148
|
+
var context = ctx || {};
|
|
149
|
+
var fire = actions || {};
|
|
150
|
+
var types = unit && Array.isArray(unit.types) ? unit.types : [];
|
|
151
|
+
if (types.length === 0) {
|
|
152
|
+
return h("div", { className: "wf-detail-empty", key: "empty" }, say("panel.page.noTypes"));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return h(
|
|
156
|
+
"div",
|
|
157
|
+
{ className: "wf-type-list", key: "types", "data-dsh-workflow-map": "type-list", "data-count": types.length },
|
|
158
|
+
h("div", { className: "wf-detail-label", key: "label" }, say("panel.page.types")),
|
|
159
|
+
h(
|
|
160
|
+
"div",
|
|
161
|
+
{ className: "wf-type-rows", key: "rows" },
|
|
162
|
+
types.map(function (type) {
|
|
163
|
+
var open = context.openType === type.key && context.openStep === unit.index;
|
|
164
|
+
return h(
|
|
165
|
+
"button",
|
|
166
|
+
{
|
|
167
|
+
type: "button",
|
|
168
|
+
className: "wf-type-row",
|
|
169
|
+
key: "type-" + type.key,
|
|
170
|
+
"data-dsh-workflow-map": "type-row",
|
|
171
|
+
"data-type": type.key,
|
|
172
|
+
"data-side": type.side,
|
|
173
|
+
"data-live": type.running === true ? "true" : "false",
|
|
174
|
+
"data-open": open ? "true" : "false",
|
|
175
|
+
"aria-pressed": open ? "true" : "false",
|
|
176
|
+
onClick: function () {
|
|
177
|
+
if (typeof fire.onOpenType === "function") fire.onOpenType(unit.index, type.key);
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
h("span", { className: "wf-type-dot", key: "dot", "data-tone": type.tone, "aria-hidden": "true" }),
|
|
181
|
+
h("span", { className: "wf-type-label", key: "label" }, type.label),
|
|
182
|
+
h("span", { className: "wf-type-kind", key: "kind" }, say(type.kind === "tool" ? "panel.page.kind.tool" : "panel.page.kind.file")),
|
|
183
|
+
h("span", { className: "wf-type-count", key: "count" }, say("panel.page.count", { count: type.count }))
|
|
184
|
+
);
|
|
185
|
+
})
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The whole right page. `extras` carries the decided content:
|
|
192
|
+
* {kind, stepIndex, unit, detail, typeKey, bucket, nodes, stepDetail, openType, openCall,
|
|
193
|
+
* activeNodeId, inject:{...}}
|
|
194
|
+
*
|
|
195
|
+
* `detail` is the record built by store.js for the SELECTED step; a page about another
|
|
196
|
+
* step falls back to the unit's own numbers and says the report is not loaded, rather than
|
|
197
|
+
* showing the selected step's report under a different step's title.
|
|
198
|
+
*/
|
|
199
|
+
function wfRenderRightPage(h, model, say, actions, extras) {
|
|
200
|
+
var fire = actions || {};
|
|
201
|
+
var ui = extras || {};
|
|
202
|
+
var kind = ui.kind === "type" || ui.kind === "inject" ? ui.kind : "step";
|
|
203
|
+
var context = {
|
|
204
|
+
stepIndex: wfFinite(ui.stepIndex) === null ? null : Math.round(ui.stepIndex),
|
|
205
|
+
scopeStep: wfFinite(ui.scopeStep) === null ? null : Math.round(ui.scopeStep),
|
|
206
|
+
openType: typeof ui.openType === "string" && ui.openType !== "" ? ui.openType : null,
|
|
207
|
+
openCall: typeof ui.openCall === "string" && ui.openCall !== "" ? ui.openCall : null,
|
|
208
|
+
openStep: wfFinite(ui.stepIndex) === null ? null : Math.round(ui.stepIndex),
|
|
209
|
+
label: typeof ui.label === "string" ? ui.label : "",
|
|
210
|
+
activeNodeId: typeof ui.activeNodeId === "string" && ui.activeNodeId !== "" ? ui.activeNodeId : null,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
var title = wfPageTitle(kind, say, {
|
|
214
|
+
label: context.label,
|
|
215
|
+
scopeStep: context.scopeStep,
|
|
216
|
+
stepIndex: context.stepIndex,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
var body;
|
|
220
|
+
if (kind === "inject") {
|
|
221
|
+
body = wfRenderHotInject(h, say, fire, {
|
|
222
|
+
sessionId: ui.sessionId,
|
|
223
|
+
state: ui.injectState,
|
|
224
|
+
text: ui.injectText,
|
|
225
|
+
reason: ui.injectReason,
|
|
226
|
+
history: ui.injectHistory,
|
|
227
|
+
// the one row being revoked (if any) plus the host's reason when it said no.
|
|
228
|
+
revokeId: ui.injectRevokeId,
|
|
229
|
+
revokeState: ui.injectRevokeState,
|
|
230
|
+
revokeReason: ui.injectRevokeReason,
|
|
231
|
+
});
|
|
232
|
+
} else if (kind === "type") {
|
|
233
|
+
body = [
|
|
234
|
+
h(
|
|
235
|
+
"div",
|
|
236
|
+
{ className: "wf-page-sub", key: "sub" },
|
|
237
|
+
say("panel.page.typeSub", {
|
|
238
|
+
step: context.scopeStep === null ? say("panel.page.wholeSession") : "第 " + (context.scopeStep + 1) + " 步",
|
|
239
|
+
count: wfFinite(ui.bucket && ui.bucket.count) === null ? 0 : Math.max(0, Math.round(ui.bucket.count)),
|
|
240
|
+
})
|
|
241
|
+
),
|
|
242
|
+
wfRenderTypeBody(h, ui.bucket, ui.nodes, say, actions, context),
|
|
243
|
+
];
|
|
244
|
+
} else {
|
|
245
|
+
var unit = ui.unit && typeof ui.unit === "object" ? ui.unit : null;
|
|
246
|
+
var stepDetail = ui.stepDetail && typeof ui.stepDetail === "object" ? ui.stepDetail : null;
|
|
247
|
+
body = [
|
|
248
|
+
unit ? wfRenderStepTypeSummary(h, unit, say, actions, context) : null,
|
|
249
|
+
stepDetail
|
|
250
|
+
? wfRenderStepDetail(h, stepDetail, say, fire, context)
|
|
251
|
+
: h("div", { className: "wf-detail-empty", key: "noload" }, say("panel.page.noReport")),
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return h(
|
|
256
|
+
"aside",
|
|
257
|
+
{
|
|
258
|
+
className: "wf-right",
|
|
259
|
+
key: "right",
|
|
260
|
+
"data-dsh-workflow-map": "right",
|
|
261
|
+
"data-kind": kind,
|
|
262
|
+
role: "region",
|
|
263
|
+
"aria-label": title,
|
|
264
|
+
},
|
|
265
|
+
h(
|
|
266
|
+
"div",
|
|
267
|
+
{ className: "wf-right-head", key: "head", "data-dsh-workflow-map": "right-head" },
|
|
268
|
+
h("span", { className: "wf-right-title", key: "title", title: title }, title),
|
|
269
|
+
wfPageClose(h, say, actions)
|
|
270
|
+
),
|
|
271
|
+
h("div", { className: "wf-right-scroll", key: "scroll", "data-dsh-workflow-map": "right-scroll" }, body),
|
|
272
|
+
// the persistent dock. Every page kind that is NOT the composer
|
|
273
|
+
// gets it at the foot, so a prompt can be typed WITHOUT opening the composer first; the
|
|
274
|
+
// composer itself does not repeat it. It reads the SAME inject state the composer reads.
|
|
275
|
+
kind === "inject"
|
|
276
|
+
? null
|
|
277
|
+
: wfRenderInjectDock(h, say, fire, {
|
|
278
|
+
sessionId: ui.sessionId,
|
|
279
|
+
state: ui.injectState,
|
|
280
|
+
text: ui.injectText,
|
|
281
|
+
reason: ui.injectReason,
|
|
282
|
+
history: ui.injectHistory,
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
}
|