@stigmer/react 3.1.9 → 3.1.11
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/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +3 -1
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/provider.d.ts +33 -1
- package/provider.d.ts.map +1 -1
- package/provider.js +4 -2
- package/provider.js.map +1 -1
- package/src/index.ts +10 -0
- package/src/provider.tsx +46 -8
- package/src/workflow/PendingApprovalsWidget.tsx +10 -3
- package/src/workflow/ReviewRendererContext.ts +90 -0
- package/src/workflow/WorkflowExecutionTimelineEvent.tsx +9 -2
- package/src/workflow/WorkflowTaskApprovalCard.tsx +32 -0
- package/src/workflow/WorkflowTaskReviewGate.tsx +232 -0
- package/src/workflow/__tests__/PendingApprovalsWidget.test.tsx +80 -0
- package/src/workflow/__tests__/WorkflowTaskApprovalCard.test.tsx +27 -0
- package/src/workflow/__tests__/WorkflowTaskReviewGate.test.tsx +283 -0
- package/src/workflow/__tests__/derive-task-detail.test.ts +78 -0
- package/src/workflow/execution-inspector/ExecutionInspector.tsx +8 -3
- package/src/workflow/execution-inspector/derive-task-detail.ts +34 -2
- package/src/workflow/index.ts +16 -0
- package/src/workflow/useReviewPayload.ts +115 -0
- package/workflow/PendingApprovalsWidget.d.ts.map +1 -1
- package/workflow/PendingApprovalsWidget.js +1 -1
- package/workflow/PendingApprovalsWidget.js.map +1 -1
- package/workflow/ReviewRendererContext.d.ts +71 -0
- package/workflow/ReviewRendererContext.d.ts.map +1 -0
- package/workflow/ReviewRendererContext.js +23 -0
- package/workflow/ReviewRendererContext.js.map +1 -0
- package/workflow/WorkflowExecutionTimelineEvent.d.ts.map +1 -1
- package/workflow/WorkflowExecutionTimelineEvent.js +7 -3
- package/workflow/WorkflowExecutionTimelineEvent.js.map +1 -1
- package/workflow/WorkflowTaskApprovalCard.d.ts +9 -0
- package/workflow/WorkflowTaskApprovalCard.d.ts.map +1 -1
- package/workflow/WorkflowTaskApprovalCard.js +8 -2
- package/workflow/WorkflowTaskApprovalCard.js.map +1 -1
- package/workflow/WorkflowTaskReviewGate.d.ts +54 -0
- package/workflow/WorkflowTaskReviewGate.d.ts.map +1 -0
- package/workflow/WorkflowTaskReviewGate.js +56 -0
- package/workflow/WorkflowTaskReviewGate.js.map +1 -0
- package/workflow/execution-inspector/ExecutionInspector.d.ts.map +1 -1
- package/workflow/execution-inspector/ExecutionInspector.js +5 -3
- package/workflow/execution-inspector/ExecutionInspector.js.map +1 -1
- package/workflow/execution-inspector/derive-task-detail.d.ts +20 -1
- package/workflow/execution-inspector/derive-task-detail.d.ts.map +1 -1
- package/workflow/execution-inspector/derive-task-detail.js +14 -0
- package/workflow/execution-inspector/derive-task-detail.js.map +1 -1
- package/workflow/index.d.ts +3 -0
- package/workflow/index.d.ts.map +1 -1
- package/workflow/index.js +4 -0
- package/workflow/index.js.map +1 -1
- package/workflow/useReviewPayload.d.ts +41 -0
- package/workflow/useReviewPayload.d.ts.map +1 -0
- package/workflow/useReviewPayload.js +79 -0
- package/workflow/useReviewPayload.js.map +1 -0
|
@@ -5,8 +5,10 @@ import type { WorkflowExecutionEvent } from "@stigmer/protos/ai/stigmer/agentic/
|
|
|
5
5
|
import { WorkflowEventType } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/event_pb";
|
|
6
6
|
import { WorkflowTaskKind } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
|
|
7
7
|
import { cn } from "@stigmer/theme";
|
|
8
|
+
import { toJson, type JsonValue } from "@bufbuild/protobuf";
|
|
9
|
+
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
8
10
|
import type { DerivedTaskState } from "../internal/store/workflow-execution-event-store.js";
|
|
9
|
-
import {
|
|
11
|
+
import { WorkflowTaskReviewGate } from "./WorkflowTaskReviewGate.js";
|
|
10
12
|
import { formatDuration, formatMicroUsd, formatBytes, formatTimestamp, formatMetaChips } from "./format-utils.js";
|
|
11
13
|
|
|
12
14
|
interface TimelineEventProps {
|
|
@@ -270,7 +272,7 @@ export const WorkflowExecutionTimelineEvent = memo(function WorkflowExecutionTim
|
|
|
270
272
|
</p>
|
|
271
273
|
)}
|
|
272
274
|
{showCard && (
|
|
273
|
-
<
|
|
275
|
+
<WorkflowTaskReviewGate
|
|
274
276
|
taskName={event.taskName}
|
|
275
277
|
prompt={p.value.prompt}
|
|
276
278
|
outcomes={(p.value.outcomes ?? []).map((o) => ({
|
|
@@ -280,6 +282,11 @@ export const WorkflowExecutionTimelineEvent = memo(function WorkflowExecutionTim
|
|
|
280
282
|
formSchema={p.value.formSchema
|
|
281
283
|
? (p.value.formSchema as unknown as Record<string, unknown>)
|
|
282
284
|
: undefined}
|
|
285
|
+
payload={p.value.payload
|
|
286
|
+
? (toJson(ValueSchema, p.value.payload) as JsonValue)
|
|
287
|
+
: null}
|
|
288
|
+
uiHint={p.value.uiHint}
|
|
289
|
+
payloadArtifactId={p.value.payloadArtifactId || null}
|
|
283
290
|
onSubmit={onSubmitTaskApproval}
|
|
284
291
|
isSubmitting={taskApprovalSubmittingTaskNames?.has(event.taskName) ?? false}
|
|
285
292
|
error={taskApprovalErrorsByTaskName?.get(event.taskName) ?? null}
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import Markdown from "react-markdown";
|
|
5
|
+
import type { JsonObject, JsonValue } from "@bufbuild/protobuf";
|
|
5
6
|
import { cn } from "@stigmer/theme";
|
|
6
7
|
import { MARKDOWN_COMPONENTS, REMARK_PLUGINS } from "../internal/markdown-components.js";
|
|
7
8
|
import { DecisionButton, type DecisionVariant } from "../internal/DecisionButton.js";
|
|
8
9
|
import { InCardDecisionError } from "../internal/InCardDecisionError.js";
|
|
10
|
+
import { formatJson } from "../execution/tool-rendering-primitives.js";
|
|
11
|
+
import { StructuredDataViewer } from "./execution-inspector/StructuredDataViewer.js";
|
|
9
12
|
|
|
10
13
|
/** Outcome descriptor for UI rendering. */
|
|
11
14
|
export interface TaskOutcome {
|
|
@@ -35,6 +38,14 @@ export interface WorkflowTaskApprovalCardProps {
|
|
|
35
38
|
* Text properties render as textareas; others as text inputs.
|
|
36
39
|
*/
|
|
37
40
|
readonly formSchema?: Record<string, unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* Resolved review payload — the material under review (issue #234).
|
|
43
|
+
* Rendered as structured data between the prompt and the form. This is
|
|
44
|
+
* the portable fallback presentation; gates whose `ui_hint` matches a
|
|
45
|
+
* registered review renderer never reach this card (see
|
|
46
|
+
* {@link WorkflowTaskReviewGate}).
|
|
47
|
+
*/
|
|
48
|
+
readonly payload?: JsonValue | null;
|
|
38
49
|
/**
|
|
39
50
|
* Called when the reviewer submits a decision.
|
|
40
51
|
* The consumer (typically {@link WorkflowExecutionViewer}) wires this
|
|
@@ -99,6 +110,7 @@ export const WorkflowTaskApprovalCard = memo(function WorkflowTaskApprovalCard({
|
|
|
99
110
|
prompt,
|
|
100
111
|
outcomes: outcomesProp,
|
|
101
112
|
formSchema,
|
|
113
|
+
payload = null,
|
|
102
114
|
onSubmit,
|
|
103
115
|
isSubmitting,
|
|
104
116
|
error,
|
|
@@ -161,6 +173,21 @@ export const WorkflowTaskApprovalCard = memo(function WorkflowTaskApprovalCard({
|
|
|
161
173
|
</div>
|
|
162
174
|
)}
|
|
163
175
|
|
|
176
|
+
{payload !== null && (
|
|
177
|
+
<div
|
|
178
|
+
aria-label={`Review material for ${taskName}`}
|
|
179
|
+
className="mb-3 max-h-96 overflow-y-auto rounded border border-border bg-background p-3"
|
|
180
|
+
>
|
|
181
|
+
{isPlainObject(payload) ? (
|
|
182
|
+
<StructuredDataViewer data={payload} />
|
|
183
|
+
) : (
|
|
184
|
+
<pre className="overflow-auto whitespace-pre-wrap break-words font-mono text-xs leading-relaxed text-foreground">
|
|
185
|
+
<code>{formatJson(payload)}</code>
|
|
186
|
+
</pre>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
|
|
164
191
|
{formFields.length > 0 && (
|
|
165
192
|
<div className="mb-3 space-y-2">
|
|
166
193
|
{formFields.map((field) => (
|
|
@@ -287,3 +314,8 @@ function resolveVariant(
|
|
|
287
314
|
function capitalize(s: string): string {
|
|
288
315
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
289
316
|
}
|
|
317
|
+
|
|
318
|
+
/** Object payloads get the structured viewer; arrays and scalars get JSON. */
|
|
319
|
+
function isPlainObject(value: JsonValue): value is JsonObject {
|
|
320
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
321
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo, useCallback } from "react";
|
|
4
|
+
import type { JsonValue } from "@bufbuild/protobuf";
|
|
5
|
+
import { cn } from "@stigmer/theme";
|
|
6
|
+
import { useReviewRenderer } from "./ReviewRendererContext.js";
|
|
7
|
+
import { useReviewPayload } from "./useReviewPayload.js";
|
|
8
|
+
import {
|
|
9
|
+
WorkflowTaskApprovalCard,
|
|
10
|
+
type TaskOutcome,
|
|
11
|
+
} from "./WorkflowTaskApprovalCard.js";
|
|
12
|
+
|
|
13
|
+
/** Props for {@link WorkflowTaskReviewGate}. */
|
|
14
|
+
export interface WorkflowTaskReviewGateProps {
|
|
15
|
+
/** Name of the human_input task awaiting a decision. */
|
|
16
|
+
readonly taskName: string;
|
|
17
|
+
/** Resolved prompt text from the task config, rendered as markdown. */
|
|
18
|
+
readonly prompt?: string;
|
|
19
|
+
/** Configured outcomes; empty falls back to Approve / Reject. */
|
|
20
|
+
readonly outcomes: readonly TaskOutcome[];
|
|
21
|
+
/** JSON Schema for the reviewer's input form, when the gate defines one. */
|
|
22
|
+
readonly formSchema?: Record<string, unknown>;
|
|
23
|
+
/** Inline review payload from the approval_requested event, or `null`. */
|
|
24
|
+
readonly payload?: JsonValue | null;
|
|
25
|
+
/** Renderer discriminator from the task config's `ui_hint`. */
|
|
26
|
+
readonly uiHint?: string;
|
|
27
|
+
/** Artifact reference when the payload was promoted, or `null`. */
|
|
28
|
+
readonly payloadArtifactId?: string | null;
|
|
29
|
+
/**
|
|
30
|
+
* Called when the reviewer submits a decision. The consumer (typically
|
|
31
|
+
* {@link WorkflowExecutionViewer}) wires this to
|
|
32
|
+
* `useWorkflowExecutionActions().submitTaskApproval`.
|
|
33
|
+
*/
|
|
34
|
+
readonly onSubmit: (
|
|
35
|
+
taskName: string,
|
|
36
|
+
outcome: string,
|
|
37
|
+
formData?: Record<string, unknown>,
|
|
38
|
+
comment?: string,
|
|
39
|
+
) => Promise<unknown>;
|
|
40
|
+
/** True while this gate's submission RPC is in flight. */
|
|
41
|
+
readonly isSubmitting: boolean;
|
|
42
|
+
/** This gate's last failed decision, or `null`. */
|
|
43
|
+
readonly error?: Error | null;
|
|
44
|
+
/** Additional CSS class names for the root container. */
|
|
45
|
+
readonly className?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The single decision surface for a pending workflow human_input gate.
|
|
50
|
+
*
|
|
51
|
+
* Owns the two review-payload concerns that sit above the approval card
|
|
52
|
+
* (issue #234):
|
|
53
|
+
*
|
|
54
|
+
* 1. **Payload materialization** — inline payloads pass straight through;
|
|
55
|
+
* artifact-backed payloads are fetched via the API-proxied
|
|
56
|
+
* `artifact.getContent` (see {@link useReviewPayload}), with loading
|
|
57
|
+
* and error states rendered here so custom renderers always receive
|
|
58
|
+
* resolved data.
|
|
59
|
+
* 2. **Renderer dispatch** — when the gate's `ui_hint` matches a renderer
|
|
60
|
+
* registered through `StigmerProvider`'s `reviewRenderers`, the host
|
|
61
|
+
* application's component presents the review; otherwise the built-in
|
|
62
|
+
* {@link WorkflowTaskApprovalCard} renders with the payload shown as
|
|
63
|
+
* structured data. An unrecognized hint never blocks the gate.
|
|
64
|
+
*
|
|
65
|
+
* Both mount points — the execution inspector's Approval tab and the
|
|
66
|
+
* timeline's inline event card — render this component, so custom
|
|
67
|
+
* renderers light up everywhere a gate appears without per-surface wiring.
|
|
68
|
+
*/
|
|
69
|
+
export const WorkflowTaskReviewGate = memo(function WorkflowTaskReviewGate({
|
|
70
|
+
taskName,
|
|
71
|
+
prompt,
|
|
72
|
+
outcomes,
|
|
73
|
+
formSchema,
|
|
74
|
+
payload = null,
|
|
75
|
+
uiHint,
|
|
76
|
+
payloadArtifactId = null,
|
|
77
|
+
onSubmit,
|
|
78
|
+
isSubmitting,
|
|
79
|
+
error,
|
|
80
|
+
className,
|
|
81
|
+
}: WorkflowTaskReviewGateProps) {
|
|
82
|
+
const Renderer = useReviewRenderer(uiHint);
|
|
83
|
+
const resolved = useReviewPayload(payload, payloadArtifactId ?? null);
|
|
84
|
+
|
|
85
|
+
const submit = useCallback(
|
|
86
|
+
(outcome: string, formData?: Record<string, unknown>, comment?: string) =>
|
|
87
|
+
onSubmit(taskName, outcome, formData, comment),
|
|
88
|
+
[onSubmit, taskName],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (resolved.isLoading) {
|
|
92
|
+
return <PayloadLoadingState taskName={taskName} className={className} />;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (resolved.error) {
|
|
96
|
+
return (
|
|
97
|
+
<PayloadErrorState
|
|
98
|
+
taskName={taskName}
|
|
99
|
+
error={resolved.error}
|
|
100
|
+
onRetry={resolved.refetch}
|
|
101
|
+
className={className}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (Renderer && resolved.payload !== null) {
|
|
107
|
+
return (
|
|
108
|
+
<div className={className}>
|
|
109
|
+
<Renderer
|
|
110
|
+
taskName={taskName}
|
|
111
|
+
payload={resolved.payload}
|
|
112
|
+
formSchema={formSchema ?? null}
|
|
113
|
+
outcomes={outcomes}
|
|
114
|
+
submit={submit}
|
|
115
|
+
isSubmitting={isSubmitting}
|
|
116
|
+
error={error ?? null}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<WorkflowTaskApprovalCard
|
|
124
|
+
taskName={taskName}
|
|
125
|
+
prompt={prompt}
|
|
126
|
+
outcomes={outcomes}
|
|
127
|
+
formSchema={formSchema}
|
|
128
|
+
payload={resolved.payload}
|
|
129
|
+
onSubmit={onSubmit}
|
|
130
|
+
isSubmitting={isSubmitting}
|
|
131
|
+
error={error}
|
|
132
|
+
className={className}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Payload resolution states (DD-006: never a blank screen)
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
function PayloadLoadingState({
|
|
142
|
+
taskName,
|
|
143
|
+
className,
|
|
144
|
+
}: {
|
|
145
|
+
readonly taskName: string;
|
|
146
|
+
readonly className?: string;
|
|
147
|
+
}) {
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
role="status"
|
|
151
|
+
aria-label={`Loading review material for ${taskName}`}
|
|
152
|
+
className={cn(
|
|
153
|
+
"mt-2 rounded-lg border border-border-prominent border-l-2 border-l-warning p-3",
|
|
154
|
+
className,
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
158
|
+
<Spinner />
|
|
159
|
+
Loading review material…
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function PayloadErrorState({
|
|
166
|
+
taskName,
|
|
167
|
+
error,
|
|
168
|
+
onRetry,
|
|
169
|
+
className,
|
|
170
|
+
}: {
|
|
171
|
+
readonly taskName: string;
|
|
172
|
+
readonly error: Error;
|
|
173
|
+
readonly onRetry: () => void;
|
|
174
|
+
readonly className?: string;
|
|
175
|
+
}) {
|
|
176
|
+
return (
|
|
177
|
+
<div
|
|
178
|
+
role="alert"
|
|
179
|
+
className={cn(
|
|
180
|
+
"mt-2 rounded-lg border border-border-prominent border-l-2 border-l-destructive p-3",
|
|
181
|
+
className,
|
|
182
|
+
)}
|
|
183
|
+
>
|
|
184
|
+
<p className="text-xs font-medium text-foreground">
|
|
185
|
+
Could not load the review material for {taskName}
|
|
186
|
+
</p>
|
|
187
|
+
<p className="mt-1 break-words text-xs text-muted-foreground">{error.message}</p>
|
|
188
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
189
|
+
The gate stays pending — retry to load the material before deciding.
|
|
190
|
+
</p>
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={onRetry}
|
|
194
|
+
className={cn(
|
|
195
|
+
"mt-2 rounded border border-border px-2 py-1 text-xs font-medium text-foreground",
|
|
196
|
+
"transition-colors hover:bg-muted",
|
|
197
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
198
|
+
)}
|
|
199
|
+
>
|
|
200
|
+
Retry
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function Spinner() {
|
|
207
|
+
return (
|
|
208
|
+
<svg
|
|
209
|
+
width="12"
|
|
210
|
+
height="12"
|
|
211
|
+
viewBox="0 0 12 12"
|
|
212
|
+
fill="none"
|
|
213
|
+
className="animate-spin"
|
|
214
|
+
aria-hidden="true"
|
|
215
|
+
>
|
|
216
|
+
<circle
|
|
217
|
+
cx="6"
|
|
218
|
+
cy="6"
|
|
219
|
+
r="5"
|
|
220
|
+
stroke="currentColor"
|
|
221
|
+
strokeWidth="1.5"
|
|
222
|
+
strokeOpacity="0.25"
|
|
223
|
+
/>
|
|
224
|
+
<path
|
|
225
|
+
d="M11 6a5 5 0 0 0-5-5"
|
|
226
|
+
stroke="currentColor"
|
|
227
|
+
strokeWidth="1.5"
|
|
228
|
+
strokeLinecap="round"
|
|
229
|
+
/>
|
|
230
|
+
</svg>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { create, type MessageInitShape } from "@bufbuild/protobuf";
|
|
4
|
+
import { PendingApprovalSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/io_pb";
|
|
5
|
+
import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/io_pb";
|
|
6
|
+
import { PendingApprovalsWidget } from "../PendingApprovalsWidget";
|
|
7
|
+
|
|
8
|
+
function makeApproval(
|
|
9
|
+
overrides: MessageInitShape<typeof PendingApprovalSchema> = {},
|
|
10
|
+
): PendingApproval {
|
|
11
|
+
return create(PendingApprovalSchema, {
|
|
12
|
+
executionId: "wfx-1",
|
|
13
|
+
workflowName: "Article Pipeline",
|
|
14
|
+
taskName: "reviewDraft",
|
|
15
|
+
requester: "usr-alice",
|
|
16
|
+
...overrides,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("PendingApprovalsWidget", () => {
|
|
21
|
+
afterEach(cleanup);
|
|
22
|
+
|
|
23
|
+
it("renders workflow name and task name", () => {
|
|
24
|
+
render(
|
|
25
|
+
<PendingApprovalsWidget
|
|
26
|
+
approvals={[makeApproval()]}
|
|
27
|
+
totalCount={1}
|
|
28
|
+
isLoading={false}
|
|
29
|
+
/>,
|
|
30
|
+
);
|
|
31
|
+
expect(screen.getByText("Article Pipeline")).toBeTruthy();
|
|
32
|
+
expect(screen.getByText(/reviewDraft/)).toBeTruthy();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("review-type badge", () => {
|
|
36
|
+
it("shows the uiHint as a badge so dashboards can distinguish review types", () => {
|
|
37
|
+
render(
|
|
38
|
+
<PendingApprovalsWidget
|
|
39
|
+
approvals={[makeApproval({ uiHint: "article-diff" })]}
|
|
40
|
+
totalCount={1}
|
|
41
|
+
isLoading={false}
|
|
42
|
+
/>,
|
|
43
|
+
);
|
|
44
|
+
expect(screen.getByText("article-diff")).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders no badge for a generic review (empty uiHint)", () => {
|
|
48
|
+
const { container } = render(
|
|
49
|
+
<PendingApprovalsWidget
|
|
50
|
+
approvals={[makeApproval()]}
|
|
51
|
+
totalCount={1}
|
|
52
|
+
isLoading={false}
|
|
53
|
+
/>,
|
|
54
|
+
);
|
|
55
|
+
// Only the workflow-name/task-name texts render; no badge span appears.
|
|
56
|
+
expect(container.querySelectorAll("span.rounded")).toHaveLength(0);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("shows the empty state when there are no approvals", () => {
|
|
61
|
+
render(
|
|
62
|
+
<PendingApprovalsWidget approvals={[]} totalCount={0} isLoading={false} />,
|
|
63
|
+
);
|
|
64
|
+
expect(screen.getByText("No approvals pending")).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("invokes onReviewClick with the execution id", () => {
|
|
68
|
+
const onReviewClick = vi.fn();
|
|
69
|
+
render(
|
|
70
|
+
<PendingApprovalsWidget
|
|
71
|
+
approvals={[makeApproval()]}
|
|
72
|
+
totalCount={1}
|
|
73
|
+
isLoading={false}
|
|
74
|
+
onReviewClick={onReviewClick}
|
|
75
|
+
/>,
|
|
76
|
+
);
|
|
77
|
+
fireEvent.click(screen.getByRole("button", { name: "Review" }));
|
|
78
|
+
expect(onReviewClick).toHaveBeenCalledWith("wfx-1");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -134,6 +134,33 @@ describe("WorkflowTaskApprovalCard", () => {
|
|
|
134
134
|
});
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
describe("review payload (issue #234 fallback display)", () => {
|
|
138
|
+
it("renders an object payload as structured data", () => {
|
|
139
|
+
render(
|
|
140
|
+
<WorkflowTaskApprovalCard
|
|
141
|
+
{...defaultProps}
|
|
142
|
+
payload={{ severity: "P1", summary: "Escalate the incident" }}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
expect(screen.getByLabelText("Review material for review_task")).toBeTruthy();
|
|
146
|
+
expect(screen.getByText("Escalate the incident")).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("renders non-object payloads as formatted JSON", () => {
|
|
150
|
+
render(
|
|
151
|
+
<WorkflowTaskApprovalCard {...defaultProps} payload={["item-1", "item-2"]} />,
|
|
152
|
+
);
|
|
153
|
+
const section = screen.getByLabelText("Review material for review_task");
|
|
154
|
+
expect(section.textContent).toContain("item-1");
|
|
155
|
+
expect(section.textContent).toContain("item-2");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("renders no review material section when payload is absent", () => {
|
|
159
|
+
render(<WorkflowTaskApprovalCard {...defaultProps} />);
|
|
160
|
+
expect(screen.queryByLabelText("Review material for review_task")).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
137
164
|
describe("submission", () => {
|
|
138
165
|
it("onSubmit called with (taskName, outcomeName, undefined, undefined) when no form or comment", () => {
|
|
139
166
|
const onSubmit = vi.fn().mockResolvedValue(undefined);
|