@burdenoff/microfe-store 2026.1028.1 → 2026.1031.1
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/components/TemplateRevisionsQueue.d.ts +10 -0
- package/dist/components/TemplateRevisionsQueue.js +300 -0
- package/dist/components/TemplateRevisionsQueue.js.map +1 -0
- package/dist/components/WorkflowTemplatePreviewPanel.d.ts +50 -0
- package/dist/components/WorkflowTemplatePreviewPanel.js +221 -0
- package/dist/components/WorkflowTemplatePreviewPanel.js.map +1 -0
- package/dist/pages/AdminSubmissionsQueuePage.js +447 -334
- package/dist/pages/AdminSubmissionsQueuePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
export declare const TEMPLATE_REVISION_COUNT_QUERY = "\n query AdminPendingWorkflowTemplateRevisionCount {\n pendingWorkflowTemplateRevisions(limit: 1) {\n totalCount\n }\n }\n";
|
|
4
|
+
export declare const TemplateRevisionsQueue: FC<{
|
|
5
|
+
authToken?: string | null;
|
|
6
|
+
/** Called with the number of pending revisions after every load. */
|
|
7
|
+
onCountChange?: (count: number) => void;
|
|
8
|
+
/** Bumped by the page's Refresh button to reload the list. */
|
|
9
|
+
refreshKey?: number;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { Markdown as e } from "../node_modules/react-markdown/lib/index.js";
|
|
2
|
+
import { WORKFLOW_TEMPLATE_PREVIEW_FIELDS as t, WorkflowTemplatePreviewPanel as n } from "./WorkflowTemplatePreviewPanel.js";
|
|
3
|
+
import { useCallback as r, useEffect as i, useRef as a, useState as o } from "react";
|
|
4
|
+
import { Check as s, ChevronDown as c, ChevronUp as l, Clock as u, GitBranch as d, Hash as f, RefreshCw as p, User as m, X as h } from "lucide-react";
|
|
5
|
+
import { graphqlFetch as g } from "@burdenoff/fe-libs/shared/graphql";
|
|
6
|
+
import { jsx as _, jsxs as v } from "react/jsx-runtime";
|
|
7
|
+
import { IllustratedEmptyState as y } from "@burdenoff/fe-libs/ui";
|
|
8
|
+
//#region src/components/TemplateRevisionsQueue.tsx
|
|
9
|
+
var b = `
|
|
10
|
+
query AdminPendingWorkflowTemplateRevisions($limit: Int, $offset: Int) {
|
|
11
|
+
pendingWorkflowTemplateRevisions(limit: $limit, offset: $offset) {
|
|
12
|
+
totalCount
|
|
13
|
+
items {
|
|
14
|
+
id
|
|
15
|
+
productId
|
|
16
|
+
productSlug
|
|
17
|
+
status
|
|
18
|
+
version
|
|
19
|
+
revision
|
|
20
|
+
liveVersion
|
|
21
|
+
name
|
|
22
|
+
description
|
|
23
|
+
category
|
|
24
|
+
tags
|
|
25
|
+
authorName
|
|
26
|
+
publisherName
|
|
27
|
+
notes
|
|
28
|
+
submittedAt
|
|
29
|
+
submittedBy
|
|
30
|
+
rejectedReason
|
|
31
|
+
nodeCount
|
|
32
|
+
preview { ${t} }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
`, x = "\n query AdminPendingWorkflowTemplateRevisionCount {\n pendingWorkflowTemplateRevisions(limit: 1) {\n totalCount\n }\n }\n", S = "\n mutation AdminApproveWorkflowTemplateRevision($id: ID!, $notes: String) {\n approveWorkflowTemplateRevision(id: $id, notes: $notes) {\n id\n status\n version\n }\n }\n", C = "\n mutation AdminRejectWorkflowTemplateRevision($id: ID!, $reason: String!) {\n rejectWorkflowTemplateRevision(id: $id, reason: $reason) {\n id\n status\n rejectedReason\n }\n }\n", w = 20;
|
|
37
|
+
function T(e) {
|
|
38
|
+
return e ? new Date(e).toLocaleString(void 0, {
|
|
39
|
+
month: "short",
|
|
40
|
+
day: "numeric",
|
|
41
|
+
year: "numeric",
|
|
42
|
+
hour: "2-digit",
|
|
43
|
+
minute: "2-digit"
|
|
44
|
+
}) : "—";
|
|
45
|
+
}
|
|
46
|
+
var E = ({ authToken: t, onCountChange: x, refreshKey: E = 0 }) => {
|
|
47
|
+
let [D, O] = o([]), [k, A] = o(!1), [j, M] = o(null), [N, P] = o(null), [F, I] = o(null), [L, R] = o(null), [z, B] = o(""), [V, H] = o(null), [U, W] = o(0), G = a(0), K = r(async (e = 0) => {
|
|
48
|
+
let n = ++G.current, r = () => n === G.current;
|
|
49
|
+
A(!0), M(null);
|
|
50
|
+
try {
|
|
51
|
+
let n = await g({
|
|
52
|
+
gateway: "global",
|
|
53
|
+
query: b,
|
|
54
|
+
variables: {
|
|
55
|
+
limit: w,
|
|
56
|
+
offset: e
|
|
57
|
+
},
|
|
58
|
+
authToken: t || void 0
|
|
59
|
+
});
|
|
60
|
+
if (!r()) return;
|
|
61
|
+
if (n.errors?.length) throw Error(n.errors[0]?.message ?? "Failed to load template revisions");
|
|
62
|
+
let i = n.data?.pendingWorkflowTemplateRevisions, a = i?.items ?? [];
|
|
63
|
+
O((t) => e > 0 ? [...t, ...a] : a), W(i?.totalCount ?? 0), x?.(i?.totalCount ?? 0);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
r() && M(e instanceof Error ? e.message : "Failed to load template revisions");
|
|
66
|
+
} finally {
|
|
67
|
+
r() && A(!1);
|
|
68
|
+
}
|
|
69
|
+
}, [t, x]);
|
|
70
|
+
i(() => {
|
|
71
|
+
K();
|
|
72
|
+
}, [K, E]);
|
|
73
|
+
let q = async (e) => {
|
|
74
|
+
P(e.id), M(null), H(null);
|
|
75
|
+
try {
|
|
76
|
+
let n = await g({
|
|
77
|
+
gateway: "global",
|
|
78
|
+
query: S,
|
|
79
|
+
variables: {
|
|
80
|
+
id: e.id,
|
|
81
|
+
notes: null
|
|
82
|
+
},
|
|
83
|
+
authToken: t || void 0
|
|
84
|
+
});
|
|
85
|
+
if (n.errors?.length) throw Error(n.errors[0]?.message ?? "Approval failed");
|
|
86
|
+
H(`${e.name} is now live at version ${n.data?.approveWorkflowTemplateRevision.version ?? e.version}.`), await K();
|
|
87
|
+
} catch (e) {
|
|
88
|
+
M(e instanceof Error ? e.message : "Approval failed");
|
|
89
|
+
} finally {
|
|
90
|
+
P(null);
|
|
91
|
+
}
|
|
92
|
+
}, J = async (e) => {
|
|
93
|
+
if (!z.trim()) {
|
|
94
|
+
M("Rejection reason is required");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
P(e.id), M(null), H(null);
|
|
98
|
+
try {
|
|
99
|
+
let n = await g({
|
|
100
|
+
gateway: "global",
|
|
101
|
+
query: C,
|
|
102
|
+
variables: {
|
|
103
|
+
id: e.id,
|
|
104
|
+
reason: z.trim()
|
|
105
|
+
},
|
|
106
|
+
authToken: t || void 0
|
|
107
|
+
});
|
|
108
|
+
if (n.errors?.length) throw Error(n.errors[0]?.message ?? "Rejection failed");
|
|
109
|
+
R(null), B(""), H(`Revision ${e.version} of ${e.name} was rejected; version ${e.liveVersion} stays live.`), await K();
|
|
110
|
+
} catch (e) {
|
|
111
|
+
M(e instanceof Error ? e.message : "Rejection failed");
|
|
112
|
+
} finally {
|
|
113
|
+
P(null);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
return k && D.length === 0 ? /* @__PURE__ */ _("div", {
|
|
117
|
+
className: "flex items-center justify-center py-24",
|
|
118
|
+
children: /* @__PURE__ */ _(p, { className: "size-6 animate-spin text-[var(--color-text-muted)]" })
|
|
119
|
+
}) : /* @__PURE__ */ v("div", {
|
|
120
|
+
className: "space-y-4",
|
|
121
|
+
children: [
|
|
122
|
+
j && /* @__PURE__ */ _("div", {
|
|
123
|
+
role: "alert",
|
|
124
|
+
className: "rounded-lg border border-[var(--color-border-danger)] bg-[var(--color-surface-danger-muted)] px-4 py-3 text-sm text-[var(--color-text-danger)]",
|
|
125
|
+
children: j
|
|
126
|
+
}),
|
|
127
|
+
V && /* @__PURE__ */ _("div", {
|
|
128
|
+
role: "status",
|
|
129
|
+
className: "rounded-lg border border-[var(--color-border-seam)] bg-[var(--color-status-success-bg-subtle)] px-4 py-3 text-sm text-[var(--color-status-success-text)]",
|
|
130
|
+
children: V
|
|
131
|
+
}),
|
|
132
|
+
D.length === 0 ? /* @__PURE__ */ _(y, {
|
|
133
|
+
illustration: "empty-data",
|
|
134
|
+
title: "No template revisions waiting",
|
|
135
|
+
description: "When an author submits a new version of a published workflow template, it appears here. The current version stays listed until you approve the new one."
|
|
136
|
+
}) : /* @__PURE__ */ _("ul", {
|
|
137
|
+
className: "space-y-4",
|
|
138
|
+
"data-testid": "template-revisions",
|
|
139
|
+
children: D.map((t) => /* @__PURE__ */ v("li", {
|
|
140
|
+
"data-testid": `template-revision-${t.id}`,
|
|
141
|
+
className: "overflow-hidden rounded-xl border border-[var(--color-border-seam)] bg-[var(--color-surface-raised)] shadow-[var(--shadow-elevation-1)]",
|
|
142
|
+
children: [
|
|
143
|
+
/* @__PURE__ */ v("div", {
|
|
144
|
+
className: "space-y-2 p-5",
|
|
145
|
+
children: [
|
|
146
|
+
/* @__PURE__ */ v("div", {
|
|
147
|
+
className: "flex flex-wrap items-center gap-2",
|
|
148
|
+
children: [
|
|
149
|
+
/* @__PURE__ */ _("h3", {
|
|
150
|
+
className: "text-base font-semibold text-[var(--color-text-primary)]",
|
|
151
|
+
children: t.name
|
|
152
|
+
}),
|
|
153
|
+
/* @__PURE__ */ v("span", {
|
|
154
|
+
className: "inline-flex items-center gap-1 rounded-full bg-[var(--color-status-info-bg-subtle)] px-2 py-0.5 text-[11px] font-semibold text-[var(--color-status-info-text)]",
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ _(d, { className: "size-3" }),
|
|
157
|
+
t.liveVersion,
|
|
158
|
+
" live → ",
|
|
159
|
+
t.version,
|
|
160
|
+
" in review"
|
|
161
|
+
]
|
|
162
|
+
}),
|
|
163
|
+
/* @__PURE__ */ _("span", {
|
|
164
|
+
className: "rounded-full bg-[var(--color-status-warning-bg-subtle)] px-2 py-0.5 text-[10px] font-medium uppercase text-[var(--color-status-warning-text)]",
|
|
165
|
+
children: "Workflow template revision"
|
|
166
|
+
})
|
|
167
|
+
]
|
|
168
|
+
}),
|
|
169
|
+
/* @__PURE__ */ v("div", {
|
|
170
|
+
className: "flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-[var(--color-text-secondary)]",
|
|
171
|
+
children: [
|
|
172
|
+
/* @__PURE__ */ v("span", {
|
|
173
|
+
className: "flex items-center gap-1",
|
|
174
|
+
children: [/* @__PURE__ */ _(f, { className: "size-3" }), /* @__PURE__ */ _("span", {
|
|
175
|
+
className: "font-mono",
|
|
176
|
+
children: t.productSlug
|
|
177
|
+
})]
|
|
178
|
+
}),
|
|
179
|
+
/* @__PURE__ */ v("span", {
|
|
180
|
+
className: "flex items-center gap-1",
|
|
181
|
+
children: [/* @__PURE__ */ _(m, { className: "size-3" }), t.authorName ?? t.publisherName ?? t.submittedBy]
|
|
182
|
+
}),
|
|
183
|
+
/* @__PURE__ */ v("span", {
|
|
184
|
+
className: "flex items-center gap-1",
|
|
185
|
+
children: [/* @__PURE__ */ _(u, { className: "size-3" }), T(t.submittedAt)]
|
|
186
|
+
}),
|
|
187
|
+
/* @__PURE__ */ v("span", { children: [
|
|
188
|
+
t.category,
|
|
189
|
+
" · ",
|
|
190
|
+
t.nodeCount,
|
|
191
|
+
" node types"
|
|
192
|
+
] })
|
|
193
|
+
]
|
|
194
|
+
}),
|
|
195
|
+
/* @__PURE__ */ _("p", {
|
|
196
|
+
className: "text-sm text-[var(--color-text-secondary)]",
|
|
197
|
+
children: t.description
|
|
198
|
+
}),
|
|
199
|
+
t.notes && /* @__PURE__ */ v("div", {
|
|
200
|
+
className: "rounded-md border border-[var(--color-border-seam)] bg-[var(--color-surface-default)] px-3 py-2 text-xs text-[var(--color-text-secondary)]",
|
|
201
|
+
children: [/* @__PURE__ */ _("span", {
|
|
202
|
+
className: "font-semibold",
|
|
203
|
+
children: "Note from the author: "
|
|
204
|
+
}), /* @__PURE__ */ _(e, { children: t.notes })]
|
|
205
|
+
})
|
|
206
|
+
]
|
|
207
|
+
}),
|
|
208
|
+
/* @__PURE__ */ v("div", {
|
|
209
|
+
className: "flex flex-wrap items-center justify-between gap-2 border-t border-[var(--color-border-seam)] bg-[var(--color-surface-muted)] px-5 py-3",
|
|
210
|
+
children: [/* @__PURE__ */ v("button", {
|
|
211
|
+
type: "button",
|
|
212
|
+
onClick: () => I(F === t.id ? null : t.id),
|
|
213
|
+
className: "inline-flex items-center gap-1.5 rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)]",
|
|
214
|
+
children: [F === t.id ? "Hide template contents" : "Preview template contents", F === t.id ? /* @__PURE__ */ _(l, { className: "size-3" }) : /* @__PURE__ */ _(c, { className: "size-3" })]
|
|
215
|
+
}), /* @__PURE__ */ v("div", {
|
|
216
|
+
className: "flex items-center gap-2",
|
|
217
|
+
children: [/* @__PURE__ */ v("button", {
|
|
218
|
+
type: "button",
|
|
219
|
+
onClick: () => void q(t),
|
|
220
|
+
disabled: N === t.id,
|
|
221
|
+
className: "inline-flex items-center gap-1.5 rounded-md bg-[var(--color-surface-success)] px-4 py-1.5 text-xs font-semibold text-[var(--color-text-on-success)] transition-opacity hover:opacity-90 disabled:opacity-50",
|
|
222
|
+
children: [/* @__PURE__ */ _(s, { className: "size-3.5" }), "Approve revision"]
|
|
223
|
+
}), /* @__PURE__ */ v("button", {
|
|
224
|
+
type: "button",
|
|
225
|
+
onClick: () => {
|
|
226
|
+
R(t.id), B("");
|
|
227
|
+
},
|
|
228
|
+
disabled: N === t.id,
|
|
229
|
+
className: "inline-flex items-center gap-1.5 rounded-md border border-[var(--color-border-danger)] bg-[var(--color-surface-danger-muted)] px-4 py-1.5 text-xs font-semibold text-[var(--color-text-danger)] transition-colors hover:opacity-90 disabled:opacity-50",
|
|
230
|
+
children: [/* @__PURE__ */ _(h, { className: "size-3.5" }), "Reject revision"]
|
|
231
|
+
})]
|
|
232
|
+
})]
|
|
233
|
+
}),
|
|
234
|
+
F === t.id && /* @__PURE__ */ _("div", {
|
|
235
|
+
className: "border-t border-[var(--color-border-seam)] bg-[var(--color-surface-muted)] p-5",
|
|
236
|
+
children: t.preview ? /* @__PURE__ */ _(n, { preview: t.preview }) : /* @__PURE__ */ _("p", {
|
|
237
|
+
className: "text-xs text-[var(--color-text-muted)]",
|
|
238
|
+
children: "This revision has no workflow definition to preview."
|
|
239
|
+
})
|
|
240
|
+
}),
|
|
241
|
+
L === t.id && /* @__PURE__ */ v("div", {
|
|
242
|
+
className: "border-t border-[var(--color-border-seam)] px-5 py-4",
|
|
243
|
+
children: [
|
|
244
|
+
/* @__PURE__ */ v("p", {
|
|
245
|
+
className: "mb-2 text-xs font-semibold text-[var(--color-text-danger)]",
|
|
246
|
+
children: [
|
|
247
|
+
"Reject this revision. Version ",
|
|
248
|
+
t.liveVersion,
|
|
249
|
+
" stays live; the author sees your reason."
|
|
250
|
+
]
|
|
251
|
+
}),
|
|
252
|
+
/* @__PURE__ */ _("textarea", {
|
|
253
|
+
"aria-label": "Revision rejection reason",
|
|
254
|
+
value: z,
|
|
255
|
+
onChange: (e) => B(e.target.value),
|
|
256
|
+
rows: 3,
|
|
257
|
+
required: !0,
|
|
258
|
+
"aria-required": !0,
|
|
259
|
+
className: "w-full rounded-lg border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-2 text-sm text-[var(--color-text-primary)] focus:border-[var(--color-border-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] resize-none",
|
|
260
|
+
placeholder: "Be specific, for example 'The new HTTP node posts to a personal endpoint'"
|
|
261
|
+
}),
|
|
262
|
+
/* @__PURE__ */ v("div", {
|
|
263
|
+
className: "mt-2 flex items-center justify-end gap-2",
|
|
264
|
+
children: [/* @__PURE__ */ _("button", {
|
|
265
|
+
type: "button",
|
|
266
|
+
onClick: () => {
|
|
267
|
+
R(null), B("");
|
|
268
|
+
},
|
|
269
|
+
className: "rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)]",
|
|
270
|
+
children: "Cancel"
|
|
271
|
+
}), /* @__PURE__ */ _("button", {
|
|
272
|
+
type: "button",
|
|
273
|
+
onClick: () => void J(t),
|
|
274
|
+
disabled: !z.trim() || N === t.id,
|
|
275
|
+
className: "rounded-md bg-[var(--color-surface-danger)] px-3 py-1.5 text-xs font-semibold text-[var(--color-text-on-danger)] hover:opacity-90 disabled:opacity-50",
|
|
276
|
+
children: "Submit rejection"
|
|
277
|
+
})]
|
|
278
|
+
})
|
|
279
|
+
]
|
|
280
|
+
})
|
|
281
|
+
]
|
|
282
|
+
}, t.id))
|
|
283
|
+
}),
|
|
284
|
+
D.length > 0 && D.length < U && /* @__PURE__ */ _("div", {
|
|
285
|
+
className: "flex justify-center",
|
|
286
|
+
children: /* @__PURE__ */ v("button", {
|
|
287
|
+
type: "button",
|
|
288
|
+
onClick: () => void K(D.length),
|
|
289
|
+
disabled: k,
|
|
290
|
+
className: "inline-flex items-center gap-2 rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-4 py-1.5 text-sm text-[var(--color-text-primary)] transition-colors hover:bg-[var(--color-surface-hover)] disabled:opacity-50",
|
|
291
|
+
children: [k && /* @__PURE__ */ _(p, { className: "size-4 animate-spin" }), "Load more"]
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
]
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
//#endregion
|
|
298
|
+
export { x as TEMPLATE_REVISION_COUNT_QUERY, E as TemplateRevisionsQueue };
|
|
299
|
+
|
|
300
|
+
//# sourceMappingURL=TemplateRevisionsQueue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplateRevisionsQueue.js","names":[],"sources":["../../src/components/TemplateRevisionsQueue.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n Check,\n ChevronDown,\n ChevronUp,\n Clock,\n GitBranch,\n Hash,\n RefreshCw,\n User,\n X,\n} from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport Markdown from 'react-markdown';\nimport {\n WORKFLOW_TEMPLATE_PREVIEW_FIELDS,\n WorkflowTemplatePreviewPanel,\n type WorkflowTemplatePreview,\n} from './WorkflowTemplatePreviewPanel';\n\n/**\n * Store admin queue of new versions of published community workflow templates\n * (BOFF-10463). The live version stays listed while a revision is reviewed;\n * approving swaps the revision in, rejecting sends the reason to the author.\n */\ntype TemplateRevision = {\n id: string;\n productId: string;\n productSlug: string;\n status: 'PENDING_REVIEW' | 'REJECTED';\n version: string;\n revision: number;\n liveVersion: string;\n name: string;\n description: string;\n category: string;\n tags: string[];\n authorName?: string | null;\n publisherName?: string | null;\n notes?: string | null;\n submittedAt: string;\n submittedBy: string;\n rejectedReason?: string | null;\n nodeCount: number;\n preview?: WorkflowTemplatePreview | null;\n};\n\nconst REVISIONS_QUERY = /* GraphQL */ `\n query AdminPendingWorkflowTemplateRevisions($limit: Int, $offset: Int) {\n pendingWorkflowTemplateRevisions(limit: $limit, offset: $offset) {\n totalCount\n items {\n id\n productId\n productSlug\n status\n version\n revision\n liveVersion\n name\n description\n category\n tags\n authorName\n publisherName\n notes\n submittedAt\n submittedBy\n rejectedReason\n nodeCount\n preview { ${WORKFLOW_TEMPLATE_PREVIEW_FIELDS} }\n }\n }\n }\n`;\n\nexport const TEMPLATE_REVISION_COUNT_QUERY = /* GraphQL */ `\n query AdminPendingWorkflowTemplateRevisionCount {\n pendingWorkflowTemplateRevisions(limit: 1) {\n totalCount\n }\n }\n`;\n\nconst APPROVE_REVISION_MUTATION = /* GraphQL */ `\n mutation AdminApproveWorkflowTemplateRevision($id: ID!, $notes: String) {\n approveWorkflowTemplateRevision(id: $id, notes: $notes) {\n id\n status\n version\n }\n }\n`;\n\nconst REJECT_REVISION_MUTATION = /* GraphQL */ `\n mutation AdminRejectWorkflowTemplateRevision($id: ID!, $reason: String!) {\n rejectWorkflowTemplateRevision(id: $id, reason: $reason) {\n id\n status\n rejectedReason\n }\n }\n`;\n\n/**\n * Revisions per page. Each carries its full preview, and the gateway caps a\n * query's cost: 20 revisions cost about 1,100 of the 2,000 allowed (100 cost 5,600).\n */\nconst PAGE_SIZE = 20;\n\nfunction formatDate(iso?: string | null): string {\n if (!iso) return '—';\n return new Date(iso).toLocaleString(undefined, {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\nexport const TemplateRevisionsQueue: FC<{\n authToken?: string | null;\n /** Called with the number of pending revisions after every load. */\n onCountChange?: (count: number) => void;\n /** Bumped by the page's Refresh button to reload the list. */\n refreshKey?: number;\n}> = ({ authToken, onCountChange, refreshKey = 0 }) => {\n const [revisions, setRevisions] = useState<TemplateRevision[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [actingId, setActingId] = useState<string | null>(null);\n const [openId, setOpenId] = useState<string | null>(null);\n const [rejectingId, setRejectingId] = useState<string | null>(null);\n const [reason, setReason] = useState('');\n const [notice, setNotice] = useState<string | null>(null);\n const [totalCount, setTotalCount] = useState(0);\n // Each load takes a ticket; only the newest load may write state, so a slow\n // response (an earlier page, or a load overtaken by a refresh) is dropped.\n const latestLoad = useRef(0);\n\n const load = useCallback(\n async (offset = 0) => {\n const ticket = ++latestLoad.current;\n const isCurrent = () => ticket === latestLoad.current;\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{\n pendingWorkflowTemplateRevisions: { totalCount: number; items: TemplateRevision[] };\n }>({\n gateway: 'global',\n query: REVISIONS_QUERY,\n variables: { limit: PAGE_SIZE, offset },\n authToken: authToken || undefined,\n });\n if (!isCurrent()) return;\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load template revisions');\n }\n const data = result.data?.pendingWorkflowTemplateRevisions;\n const page = data?.items ?? [];\n setRevisions((prev) => (offset > 0 ? [...prev, ...page] : page));\n setTotalCount(data?.totalCount ?? 0);\n onCountChange?.(data?.totalCount ?? 0);\n } catch (err) {\n if (isCurrent()) {\n setError(err instanceof Error ? err.message : 'Failed to load template revisions');\n }\n } finally {\n if (isCurrent()) setLoading(false);\n }\n },\n [authToken, onCountChange]\n );\n\n useEffect(() => {\n void load();\n }, [load, refreshKey]);\n\n const approve = async (revision: TemplateRevision) => {\n setActingId(revision.id);\n setError(null);\n setNotice(null);\n try {\n const result = await graphqlFetch<{\n approveWorkflowTemplateRevision: { id: string; version: string };\n }>({\n gateway: 'global',\n query: APPROVE_REVISION_MUTATION,\n variables: { id: revision.id, notes: null },\n authToken: authToken || undefined,\n });\n if (result.errors?.length) throw new Error(result.errors[0]?.message ?? 'Approval failed');\n setNotice(\n `${revision.name} is now live at version ${\n result.data?.approveWorkflowTemplateRevision.version ?? revision.version\n }.`\n );\n await load();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Approval failed');\n } finally {\n setActingId(null);\n }\n };\n\n const reject = async (revision: TemplateRevision) => {\n if (!reason.trim()) {\n setError('Rejection reason is required');\n return;\n }\n setActingId(revision.id);\n setError(null);\n setNotice(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: REJECT_REVISION_MUTATION,\n variables: { id: revision.id, reason: reason.trim() },\n authToken: authToken || undefined,\n });\n if (result.errors?.length) throw new Error(result.errors[0]?.message ?? 'Rejection failed');\n setRejectingId(null);\n setReason('');\n setNotice(\n `Revision ${revision.version} of ${revision.name} was rejected; version ${revision.liveVersion} stays live.`\n );\n await load();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Rejection failed');\n } finally {\n setActingId(null);\n }\n };\n\n if (loading && revisions.length === 0) {\n return (\n <div className=\"flex items-center justify-center py-24\">\n <RefreshCw className=\"size-6 animate-spin text-[var(--color-text-muted)]\" />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-4\">\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-lg border border-[var(--color-border-danger)] bg-[var(--color-surface-danger-muted)] px-4 py-3 text-sm text-[var(--color-text-danger)]\"\n >\n {error}\n </div>\n )}\n {notice && (\n <div\n role=\"status\"\n className=\"rounded-lg border border-[var(--color-border-seam)] bg-[var(--color-status-success-bg-subtle)] px-4 py-3 text-sm text-[var(--color-status-success-text)]\"\n >\n {notice}\n </div>\n )}\n {revisions.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No template revisions waiting\"\n description=\"When an author submits a new version of a published workflow template, it appears here. The current version stays listed until you approve the new one.\"\n />\n ) : (\n <ul className=\"space-y-4\" data-testid=\"template-revisions\">\n {revisions.map((revision) => (\n <li\n key={revision.id}\n data-testid={`template-revision-${revision.id}`}\n className=\"overflow-hidden rounded-xl border border-[var(--color-border-seam)] bg-[var(--color-surface-raised)] shadow-[var(--shadow-elevation-1)]\"\n >\n <div className=\"space-y-2 p-5\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h3 className=\"text-base font-semibold text-[var(--color-text-primary)]\">\n {revision.name}\n </h3>\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-status-info-bg-subtle)] px-2 py-0.5 text-[11px] font-semibold text-[var(--color-status-info-text)]\">\n <GitBranch className=\"size-3\" />\n {revision.liveVersion} live → {revision.version} in review\n </span>\n <span className=\"rounded-full bg-[var(--color-status-warning-bg-subtle)] px-2 py-0.5 text-[10px] font-medium uppercase text-[var(--color-status-warning-text)]\">\n Workflow template revision\n </span>\n </div>\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-[var(--color-text-secondary)]\">\n <span className=\"flex items-center gap-1\">\n <Hash className=\"size-3\" />\n <span className=\"font-mono\">{revision.productSlug}</span>\n </span>\n <span className=\"flex items-center gap-1\">\n <User className=\"size-3\" />\n {revision.authorName ?? revision.publisherName ?? revision.submittedBy}\n </span>\n <span className=\"flex items-center gap-1\">\n <Clock className=\"size-3\" />\n {formatDate(revision.submittedAt)}\n </span>\n <span>\n {revision.category} · {revision.nodeCount} node types\n </span>\n </div>\n <p className=\"text-sm text-[var(--color-text-secondary)]\">{revision.description}</p>\n {revision.notes && (\n <div className=\"rounded-md border border-[var(--color-border-seam)] bg-[var(--color-surface-default)] px-3 py-2 text-xs text-[var(--color-text-secondary)]\">\n <span className=\"font-semibold\">Note from the author: </span>\n <Markdown>{revision.notes}</Markdown>\n </div>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center justify-between gap-2 border-t border-[var(--color-border-seam)] bg-[var(--color-surface-muted)] px-5 py-3\">\n <button\n type=\"button\"\n onClick={() => setOpenId(openId === revision.id ? null : revision.id)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)]\"\n >\n {openId === revision.id ? 'Hide template contents' : 'Preview template contents'}\n {openId === revision.id ? (\n <ChevronUp className=\"size-3\" />\n ) : (\n <ChevronDown className=\"size-3\" />\n )}\n </button>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => void approve(revision)}\n disabled={actingId === revision.id}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-[var(--color-surface-success)] px-4 py-1.5 text-xs font-semibold text-[var(--color-text-on-success)] transition-opacity hover:opacity-90 disabled:opacity-50\"\n >\n <Check className=\"size-3.5\" />\n Approve revision\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setRejectingId(revision.id);\n setReason('');\n }}\n disabled={actingId === revision.id}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-[var(--color-border-danger)] bg-[var(--color-surface-danger-muted)] px-4 py-1.5 text-xs font-semibold text-[var(--color-text-danger)] transition-colors hover:opacity-90 disabled:opacity-50\"\n >\n <X className=\"size-3.5\" />\n Reject revision\n </button>\n </div>\n </div>\n\n {openId === revision.id && (\n <div className=\"border-t border-[var(--color-border-seam)] bg-[var(--color-surface-muted)] p-5\">\n {revision.preview ? (\n <WorkflowTemplatePreviewPanel preview={revision.preview} />\n ) : (\n <p className=\"text-xs text-[var(--color-text-muted)]\">\n This revision has no workflow definition to preview.\n </p>\n )}\n </div>\n )}\n\n {rejectingId === revision.id && (\n <div className=\"border-t border-[var(--color-border-seam)] px-5 py-4\">\n <p className=\"mb-2 text-xs font-semibold text-[var(--color-text-danger)]\">\n Reject this revision. Version {revision.liveVersion} stays live; the author sees\n your reason.\n </p>\n <textarea\n aria-label=\"Revision rejection reason\"\n value={reason}\n onChange={(e) => setReason(e.target.value)}\n rows={3}\n required\n aria-required\n className=\"w-full rounded-lg border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-2 text-sm text-[var(--color-text-primary)] focus:border-[var(--color-border-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] resize-none\"\n placeholder=\"Be specific, for example 'The new HTTP node posts to a personal endpoint'\"\n />\n <div className=\"mt-2 flex items-center justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setRejectingId(null);\n setReason('');\n }}\n className=\"rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)]\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={() => void reject(revision)}\n disabled={!reason.trim() || actingId === revision.id}\n className=\"rounded-md bg-[var(--color-surface-danger)] px-3 py-1.5 text-xs font-semibold text-[var(--color-text-on-danger)] hover:opacity-90 disabled:opacity-50\"\n >\n Submit rejection\n </button>\n </div>\n </div>\n )}\n </li>\n ))}\n </ul>\n )}\n {revisions.length > 0 && revisions.length < totalCount && (\n <div className=\"flex justify-center\">\n <button\n type=\"button\"\n onClick={() => void load(revisions.length)}\n disabled={loading}\n className=\"inline-flex items-center gap-2 rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-4 py-1.5 text-sm text-[var(--color-text-primary)] transition-colors hover:bg-[var(--color-surface-hover)] disabled:opacity-50\"\n >\n {loading && <RefreshCw className=\"size-4 animate-spin\" />}\n Load more\n </button>\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reviewer preview of a community workflow template (BOFF-10463): what the
|
|
5
|
+
* template does before a store admin approves it. Mirrors the store service's
|
|
6
|
+
* `WorkflowTemplatePreview` type.
|
|
7
|
+
*/
|
|
8
|
+
export type WorkflowTemplateFieldRef = {
|
|
9
|
+
nodeId?: string | null;
|
|
10
|
+
nodeType?: string | null;
|
|
11
|
+
nodeLabel?: string | null;
|
|
12
|
+
field: string;
|
|
13
|
+
};
|
|
14
|
+
export type WorkflowTemplatePreview = {
|
|
15
|
+
nodeTypes: Array<{
|
|
16
|
+
nodeType: string;
|
|
17
|
+
nodeVersion: string;
|
|
18
|
+
publishType: string;
|
|
19
|
+
displayName: string;
|
|
20
|
+
}>;
|
|
21
|
+
nodeCount: number;
|
|
22
|
+
edgeCount: number;
|
|
23
|
+
requiredInputs: Array<{
|
|
24
|
+
name: string;
|
|
25
|
+
type: string;
|
|
26
|
+
label: string;
|
|
27
|
+
required: boolean;
|
|
28
|
+
usedBy: string[];
|
|
29
|
+
}>;
|
|
30
|
+
requiredCredentials: Array<{
|
|
31
|
+
name: string;
|
|
32
|
+
kind: string;
|
|
33
|
+
nodeId?: string | null;
|
|
34
|
+
nodeType?: string | null;
|
|
35
|
+
nodeLabel?: string | null;
|
|
36
|
+
}>;
|
|
37
|
+
cleaningReport?: {
|
|
38
|
+
connectionsCleared: number;
|
|
39
|
+
secretsRemoved: number;
|
|
40
|
+
pinnedDataRemoved: boolean;
|
|
41
|
+
clearedConnections: WorkflowTemplateFieldRef[];
|
|
42
|
+
removedSecrets: WorkflowTemplateFieldRef[];
|
|
43
|
+
} | null;
|
|
44
|
+
definitionJson?: Record<string, unknown> | null;
|
|
45
|
+
};
|
|
46
|
+
/** GraphQL selection for a `WorkflowTemplatePreview` field. */
|
|
47
|
+
export declare const WORKFLOW_TEMPLATE_PREVIEW_FIELDS = "\n nodeTypes { nodeType nodeVersion publishType displayName }\n nodeCount\n edgeCount\n requiredInputs { name type label required usedBy }\n requiredCredentials { name kind nodeId nodeType nodeLabel }\n cleaningReport {\n connectionsCleared\n secretsRemoved\n pinnedDataRemoved\n clearedConnections { nodeId nodeType nodeLabel field }\n removedSecrets { nodeId nodeType nodeLabel field }\n }\n definitionJson\n";
|
|
48
|
+
export declare const WorkflowTemplatePreviewPanel: FC<{
|
|
49
|
+
preview: WorkflowTemplatePreview;
|
|
50
|
+
}>;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { useState as e } from "react";
|
|
2
|
+
import { ChevronDown as t, ChevronUp as n, Eraser as r, FileJson as i, KeyRound as a, Network as o, PlugZap as s, TextCursorInput as c } from "lucide-react";
|
|
3
|
+
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
4
|
+
//#region src/components/WorkflowTemplatePreviewPanel.tsx
|
|
5
|
+
var d = "\n nodeTypes { nodeType nodeVersion publishType displayName }\n nodeCount\n edgeCount\n requiredInputs { name type label required usedBy }\n requiredCredentials { name kind nodeId nodeType nodeLabel }\n cleaningReport {\n connectionsCleared\n secretsRemoved\n pinnedDataRemoved\n clearedConnections { nodeId nodeType nodeLabel field }\n removedSecrets { nodeId nodeType nodeLabel field }\n }\n definitionJson\n";
|
|
6
|
+
function f(e) {
|
|
7
|
+
return e.nodeLabel ?? e.nodeId ?? "Workflow";
|
|
8
|
+
}
|
|
9
|
+
var p = "mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]", m = "inline-flex items-center gap-1 rounded-md border border-[var(--color-border-seam)] bg-[var(--color-surface-default)] px-2 py-1 text-xs text-[var(--color-text-primary)]", h = ({ refs: e, total: t, testId: n }) => /* @__PURE__ */ u("ul", {
|
|
10
|
+
className: "mt-1.5 space-y-1",
|
|
11
|
+
"data-testid": n,
|
|
12
|
+
children: [e.map((e) => /* @__PURE__ */ u("li", {
|
|
13
|
+
className: "text-xs text-[var(--color-text-secondary)]",
|
|
14
|
+
children: [
|
|
15
|
+
/* @__PURE__ */ l("span", {
|
|
16
|
+
className: "font-medium text-[var(--color-text-primary)]",
|
|
17
|
+
children: f(e)
|
|
18
|
+
}),
|
|
19
|
+
e.nodeType && /* @__PURE__ */ u("span", {
|
|
20
|
+
className: "text-[var(--color-text-muted)]",
|
|
21
|
+
children: [
|
|
22
|
+
" (",
|
|
23
|
+
e.nodeType,
|
|
24
|
+
")"
|
|
25
|
+
]
|
|
26
|
+
}),
|
|
27
|
+
/* @__PURE__ */ u("span", {
|
|
28
|
+
className: "font-mono",
|
|
29
|
+
children: [" · ", e.field]
|
|
30
|
+
})
|
|
31
|
+
]
|
|
32
|
+
}, `${e.nodeId ?? "root"}:${e.field}`)), t > e.length && /* @__PURE__ */ u("li", {
|
|
33
|
+
className: "text-xs text-[var(--color-text-muted)]",
|
|
34
|
+
children: [
|
|
35
|
+
"and ",
|
|
36
|
+
t - e.length,
|
|
37
|
+
" more not listed"
|
|
38
|
+
]
|
|
39
|
+
})]
|
|
40
|
+
}), g = "text-xs text-[var(--color-text-muted)]", _ = "text-xs text-[var(--color-text-secondary)]";
|
|
41
|
+
function v(e, t) {
|
|
42
|
+
return `${e} ${t}${e === 1 ? "" : "s"}`;
|
|
43
|
+
}
|
|
44
|
+
var y = ({ preview: e }) => /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ u("h4", {
|
|
45
|
+
className: p,
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ l(o, { className: "size-3.5" }),
|
|
48
|
+
"Nodes (",
|
|
49
|
+
v(e.nodeCount, "node"),
|
|
50
|
+
", ",
|
|
51
|
+
v(e.edgeCount, "connection"),
|
|
52
|
+
",",
|
|
53
|
+
" ",
|
|
54
|
+
v(e.nodeTypes.length, "node type"),
|
|
55
|
+
")"
|
|
56
|
+
]
|
|
57
|
+
}), e.nodeTypes.length === 0 ? /* @__PURE__ */ l("p", {
|
|
58
|
+
className: g,
|
|
59
|
+
children: "No node types found."
|
|
60
|
+
}) : /* @__PURE__ */ l("div", {
|
|
61
|
+
className: "flex flex-wrap gap-1.5",
|
|
62
|
+
"data-testid": "template-node-types",
|
|
63
|
+
children: e.nodeTypes.map((e) => /* @__PURE__ */ u("span", {
|
|
64
|
+
className: m,
|
|
65
|
+
title: e.nodeType,
|
|
66
|
+
children: [
|
|
67
|
+
e.displayName,
|
|
68
|
+
/* @__PURE__ */ u("span", {
|
|
69
|
+
className: "font-mono text-[var(--color-text-muted)]",
|
|
70
|
+
children: [
|
|
71
|
+
e.nodeType,
|
|
72
|
+
" · v",
|
|
73
|
+
e.nodeVersion
|
|
74
|
+
]
|
|
75
|
+
}),
|
|
76
|
+
e.publishType === "STORE" && /* @__PURE__ */ l("span", {
|
|
77
|
+
className: "rounded bg-[var(--color-status-info-bg-subtle)] px-1 text-[10px] text-[var(--color-status-info-text)]",
|
|
78
|
+
children: "Store"
|
|
79
|
+
})
|
|
80
|
+
]
|
|
81
|
+
}, e.nodeType))
|
|
82
|
+
})] }), b = ({ inputs: e }) => /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ u("h4", {
|
|
83
|
+
className: p,
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ l(c, { className: "size-3.5" }),
|
|
86
|
+
"Required inputs (",
|
|
87
|
+
e.length,
|
|
88
|
+
")"
|
|
89
|
+
]
|
|
90
|
+
}), e.length === 0 ? /* @__PURE__ */ l("p", {
|
|
91
|
+
className: g,
|
|
92
|
+
children: "The template reads no variables the importer has to fill in."
|
|
93
|
+
}) : /* @__PURE__ */ l("ul", {
|
|
94
|
+
className: "space-y-1",
|
|
95
|
+
"data-testid": "template-required-inputs",
|
|
96
|
+
children: e.map((e) => /* @__PURE__ */ u("li", {
|
|
97
|
+
className: _,
|
|
98
|
+
children: [
|
|
99
|
+
/* @__PURE__ */ l("span", {
|
|
100
|
+
className: "font-mono text-[var(--color-text-primary)]",
|
|
101
|
+
children: e.name
|
|
102
|
+
}),
|
|
103
|
+
e.type === "secret" && /* @__PURE__ */ l("span", {
|
|
104
|
+
className: "ml-1 rounded bg-[var(--color-status-warning-bg-subtle)] px-1 text-[10px] text-[var(--color-status-warning-text)]",
|
|
105
|
+
children: "secret"
|
|
106
|
+
}),
|
|
107
|
+
e.usedBy.length > 0 && /* @__PURE__ */ u("span", { children: [" · used by ", e.usedBy.join(", ")] })
|
|
108
|
+
]
|
|
109
|
+
}, e.name))
|
|
110
|
+
})] }), x = ({ credentials: e }) => /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ u("h4", {
|
|
111
|
+
className: p,
|
|
112
|
+
children: [
|
|
113
|
+
/* @__PURE__ */ l(s, { className: "size-3.5" }),
|
|
114
|
+
"Required credentials (",
|
|
115
|
+
e.length,
|
|
116
|
+
")"
|
|
117
|
+
]
|
|
118
|
+
}), e.length === 0 ? /* @__PURE__ */ l("p", {
|
|
119
|
+
className: g,
|
|
120
|
+
children: "No credentials needed."
|
|
121
|
+
}) : /* @__PURE__ */ l("ul", {
|
|
122
|
+
className: "space-y-1",
|
|
123
|
+
"data-testid": "template-required-credentials",
|
|
124
|
+
children: e.map((e) => {
|
|
125
|
+
let t = e.nodeLabel ?? e.nodeId, n = e.kind === "connection" ? "saved connection (cleared at submission)" : "credential lookup";
|
|
126
|
+
return /* @__PURE__ */ u("li", {
|
|
127
|
+
className: _,
|
|
128
|
+
children: [
|
|
129
|
+
/* @__PURE__ */ l("span", {
|
|
130
|
+
className: "font-medium text-[var(--color-text-primary)]",
|
|
131
|
+
children: e.name
|
|
132
|
+
}),
|
|
133
|
+
/* @__PURE__ */ u("span", { children: [" · ", n] }),
|
|
134
|
+
t && /* @__PURE__ */ u("span", { children: [" · ", t] })
|
|
135
|
+
]
|
|
136
|
+
}, `${e.kind}-${e.name}-${e.nodeId ?? ""}`);
|
|
137
|
+
})
|
|
138
|
+
})] }), S = ({ report: e }) => /* @__PURE__ */ u("div", {
|
|
139
|
+
className: "space-y-3 text-xs",
|
|
140
|
+
"data-testid": "template-cleaning-report",
|
|
141
|
+
children: [
|
|
142
|
+
/* @__PURE__ */ u("div", {
|
|
143
|
+
className: "flex flex-wrap gap-1.5",
|
|
144
|
+
children: [
|
|
145
|
+
/* @__PURE__ */ u("span", {
|
|
146
|
+
className: m,
|
|
147
|
+
children: [
|
|
148
|
+
/* @__PURE__ */ l(s, { className: "size-3" }),
|
|
149
|
+
v(e.connectionsCleared, "connection"),
|
|
150
|
+
" cleared"
|
|
151
|
+
]
|
|
152
|
+
}),
|
|
153
|
+
/* @__PURE__ */ u("span", {
|
|
154
|
+
className: m,
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ l(a, { className: "size-3" }),
|
|
157
|
+
v(e.secretsRemoved, "secret"),
|
|
158
|
+
" removed"
|
|
159
|
+
]
|
|
160
|
+
}),
|
|
161
|
+
/* @__PURE__ */ l("span", {
|
|
162
|
+
className: m,
|
|
163
|
+
children: e.pinnedDataRemoved ? "Pinned run data removed" : "No pinned run data"
|
|
164
|
+
})
|
|
165
|
+
]
|
|
166
|
+
}),
|
|
167
|
+
e.clearedConnections.length > 0 && /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ l("p", {
|
|
168
|
+
className: "font-medium text-[var(--color-text-secondary)]",
|
|
169
|
+
children: "Connections cleared"
|
|
170
|
+
}), /* @__PURE__ */ l(h, {
|
|
171
|
+
refs: e.clearedConnections,
|
|
172
|
+
total: e.connectionsCleared,
|
|
173
|
+
testId: "template-cleared-connections"
|
|
174
|
+
})] }),
|
|
175
|
+
e.removedSecrets.length > 0 && /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ l("p", {
|
|
176
|
+
className: "font-medium text-[var(--color-text-secondary)]",
|
|
177
|
+
children: "Secrets removed"
|
|
178
|
+
}), /* @__PURE__ */ l(h, {
|
|
179
|
+
refs: e.removedSecrets,
|
|
180
|
+
total: e.secretsRemoved,
|
|
181
|
+
testId: "template-removed-secrets"
|
|
182
|
+
})] })
|
|
183
|
+
]
|
|
184
|
+
}), C = ({ report: e }) => /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ u("h4", {
|
|
185
|
+
className: p,
|
|
186
|
+
children: [/* @__PURE__ */ l(r, { className: "size-3.5" }), "Cleaning report"]
|
|
187
|
+
}), e ? /* @__PURE__ */ l(S, { report: e }) : /* @__PURE__ */ l("p", {
|
|
188
|
+
className: g,
|
|
189
|
+
children: "No cleaning report was recorded for this template (it was stored before reports were kept)."
|
|
190
|
+
})] }), w = ({ definition: r }) => {
|
|
191
|
+
let [a, o] = e(!1);
|
|
192
|
+
return /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ u("button", {
|
|
193
|
+
type: "button",
|
|
194
|
+
onClick: () => o((e) => !e),
|
|
195
|
+
className: "inline-flex items-center gap-1.5 rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)]",
|
|
196
|
+
children: [
|
|
197
|
+
/* @__PURE__ */ l(i, { className: "size-3.5" }),
|
|
198
|
+
a ? "Hide definition" : "View cleaned definition",
|
|
199
|
+
l(a ? n : t, { className: "size-3" })
|
|
200
|
+
]
|
|
201
|
+
}), a && /* @__PURE__ */ l("pre", {
|
|
202
|
+
className: "mt-2 max-h-72 overflow-auto rounded-lg border border-[var(--color-border-seam)] bg-[var(--color-surface-default)] p-4 text-[11px] leading-relaxed text-[var(--color-text-primary)]",
|
|
203
|
+
children: JSON.stringify(r, null, 2)
|
|
204
|
+
})] });
|
|
205
|
+
}, T = ({ preview: e }) => /* @__PURE__ */ u("div", {
|
|
206
|
+
className: "space-y-5",
|
|
207
|
+
"data-testid": "workflow-template-preview",
|
|
208
|
+
children: [
|
|
209
|
+
/* @__PURE__ */ l(y, { preview: e }),
|
|
210
|
+
/* @__PURE__ */ u("div", {
|
|
211
|
+
className: "grid gap-5 sm:grid-cols-2",
|
|
212
|
+
children: [/* @__PURE__ */ l(b, { inputs: e.requiredInputs }), /* @__PURE__ */ l(x, { credentials: e.requiredCredentials })]
|
|
213
|
+
}),
|
|
214
|
+
/* @__PURE__ */ l(C, { report: e.cleaningReport }),
|
|
215
|
+
e.definitionJson && /* @__PURE__ */ l(w, { definition: e.definitionJson })
|
|
216
|
+
]
|
|
217
|
+
});
|
|
218
|
+
//#endregion
|
|
219
|
+
export { d as WORKFLOW_TEMPLATE_PREVIEW_FIELDS, T as WorkflowTemplatePreviewPanel };
|
|
220
|
+
|
|
221
|
+
//# sourceMappingURL=WorkflowTemplatePreviewPanel.js.map
|