@burdenoff/fluidgrids-fe-sdk 2026.1112.8 → 2026.1112.9

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.
Files changed (43) hide show
  1. package/dist/{AiStudioPage-CtgPSPyc.js → AiStudioPage-DfoV1Olc.js} +1 -1
  2. package/dist/ApprovalInboxPage-Bb8H9ruw.js +1317 -0
  3. package/dist/{DataTablesPage-2xTLe_jk.js → DataTablesPage-Zz23QZQp.js} +3 -1
  4. package/dist/Disclosure-DQiUKrqd.js +41 -0
  5. package/dist/{ExecutionDetailPage-CL-w9c3L.js → ExecutionDetailPage-CcUxFexZ.js} +3 -2
  6. package/dist/{ExecutionsListPage-Bf6Cvsnz.js → ExecutionsListPage-JpByw6nA.js} +3 -1
  7. package/dist/{MissingNodesInstallModal-BCbhddk4.js → MissingNodesInstallModal-olEc52p2.js} +1 -1
  8. package/dist/{OpenTemplateAsCodeButton-BgvgDaVe.js → OpenTemplateAsCodeButton-Dh3ee2OR.js} +1 -1
  9. package/dist/PaidTemplateConfirm-DjmVi_m-.js +28 -0
  10. package/dist/{RunActionsMenu-DmO14hEE.js → RunActionsMenu-BE_1r3bF.js} +1 -50
  11. package/dist/{RunnersPage-CvAPHdfU.js → RunnersPage-CAt4YDhB.js} +2 -1
  12. package/dist/TechnicalDetails-D4xGy201.js +19 -0
  13. package/dist/{TemplateCard-XBc0qEA4.js → TemplateCard--WYJZlA_.js} +81 -25
  14. package/dist/{TemplateConfigurationModal-Dl_7RaCt.js → TemplateConfigurationModal-cd0-QVMZ.js} +2 -2
  15. package/dist/{TemplateDetailPanel-0Cc6jguU.js → TemplateDetailPanel-C-2JuXg6.js} +1 -1
  16. package/dist/TemplatePreview-BCnGjMuN.js +646 -0
  17. package/dist/{TemplatesPage-wClKZLO4.js → TemplatesPage-u7bXJYfv.js} +375 -136
  18. package/dist/{VariablesPage-BtoLjmUA.js → VariablesPage-CPL34WBc.js} +2 -358
  19. package/dist/{WorkflowBuilder-qRGFtXJG.js → WorkflowBuilder-BWxBmBoP.js} +2 -2
  20. package/dist/{WorkflowBuilder-CK6c0eRp.js → WorkflowBuilder-DxAAPHsn.js} +3 -3
  21. package/dist/{agentTools-BkQsycAp.js → agentTools-JHtYGgd7.js} +2 -2
  22. package/dist/{canvasNoteTools-U7j1z7GR.js → canvasNoteTools-B2ouyIbj.js} +1 -1
  23. package/dist/{HumanRequestCard-lbWDpWd7.js → channel-CZ70oI81.js} +5 -228
  24. package/dist/executions.js +2 -2
  25. package/dist/{flow-builder-9jDNwkID.js → flow-builder-DzKPrW2r.js} +33 -24
  26. package/dist/{index-rj-TznII.d.ts → index-CGCAcRKo.d.ts} +3 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +19 -19
  29. package/dist/{node-detail-view-C1yupyvL.js → node-detail-view-DNilUGcx.js} +2 -2
  30. package/dist/{node-editor-tab-body-BtxSQPtX.js → node-editor-tab-body-XBedxkpN.js} +1 -1
  31. package/dist/observability.d.ts +1 -1
  32. package/dist/{popover-BaYffpTw.js → popover-BC4r84K0.js} +1 -1
  33. package/dist/{quick-config-panel-BvlhLiQp.js → quick-config-panel-BPmrhIOb.js} +2 -2
  34. package/dist/{schedule-panel-D5VdzW2c.js → schedule-panel-DJN1TDQH.js} +2 -2
  35. package/dist/{sidebar-node-editor-BU4rJwvm.js → sidebar-node-editor-DKdfcd2r.js} +2 -2
  36. package/dist/templates.js +2 -2
  37. package/dist/useVariables-cI8uLwye.js +361 -0
  38. package/dist/workflow-builder.js +2 -2
  39. package/dist/workflow-pages.d.ts +1 -1
  40. package/dist/workflow-pages.js +10 -10
  41. package/package.json +1 -1
  42. package/dist/ApprovalInboxPage-QAJim81n.js +0 -559
  43. package/dist/TemplatePreview-CRLUX5yI.js +0 -240
@@ -0,0 +1,646 @@
1
+ "use client";
2
+ import { t as __exportAll } from "./rolldown-runtime-hDbaFkMy.js";
3
+ import { t as ThemeProvider } from "./theme-provider-B3mIuz2d.js";
4
+ import { t as FlowBuilderPage } from "./flow-builder-DzKPrW2r.js";
5
+ import { S as useInstalledNodes, c as buildNodeCheck, d as useImportWorkflowTemplate, l as buildRequiredNodeTypes, n as formatTemplatePrice, r as isPaidTemplate, s as useTemplateImportFlow } from "./TemplateCard--WYJZlA_.js";
6
+ import { S as QuotaExhaustedDialog } from "./workflow-code-transfer-Cshsgf2H.js";
7
+ import { n as WithOpenTemplateAsCode } from "./OpenTemplateAsCodeButton-Dh3ee2OR.js";
8
+ import { r as useTr } from "./TranslationBridge-Da-TPgQm.js";
9
+ import { i as SdkPermissionButton, s as useFluidGridsPermission, t as FLUIDGRIDS_PERMISSIONS } from "./constants-dcxg6zG6.js";
10
+ import { t as BILLING_PLANS_PATH } from "./quota-Cdt8yz7N.js";
11
+ import { n as useNavigationV2 } from "./useNavigationV2-C2WeqN7d.js";
12
+ import { m as describeOperationError } from "./client-C9XSzSXr.js";
13
+ import { t as Disclosure } from "./Disclosure-DQiUKrqd.js";
14
+ import { c as useVariables, t as useCreateVariable } from "./useVariables-cI8uLwye.js";
15
+ import { t as PaidTemplateConfirm } from "./PaidTemplateConfirm-DjmVi_m-.js";
16
+ import { useEffect, useMemo, useState } from "react";
17
+ import { useLocation, useNavigate, useParams } from "react-router";
18
+ import { Icon } from "@iconify/react";
19
+ import { jsx, jsxs } from "react/jsx-runtime";
20
+ import { toast } from "sonner";
21
+ import { Input } from "@burdenoff/fe-libs/ui";
22
+ import { ErrorBoundary } from "@burdenoff/fe-libs/shared/components";
23
+ import { Button as Button$1 } from "@burdenoff/fe-libs/button";
24
+ import "@xyflow/react/dist/style.css";
25
+
26
+ // src/utils/template-needs.ts
27
+ /** "slack_oauth2" → "Slack oauth2", "google-sheets" → "Google sheets". */
28
+ function humanizeIdentifier(identifier) {
29
+ const text = identifier.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_\-.:/]+/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
30
+ return text ? text.charAt(0).toUpperCase() + text.slice(1) : identifier;
31
+ }
32
+ function appName(node) {
33
+ return node.displayName?.trim() || humanizeIdentifier(node.nodeType);
34
+ }
35
+ /**
36
+ * The store apps the template uses (built-in steps are always there, so they
37
+ * are not listed), each with its status: ready (installed), installed for
38
+ * you when you use the template, to buy first, or not in the store.
39
+ */
40
+ function templateApps(check) {
41
+ const entry = (status) => (node) => ({
42
+ nodeType: node.nodeType,
43
+ name: appName(node),
44
+ status
45
+ });
46
+ return [
47
+ ...check.installed.filter((node) => node.publishType !== "BUILTIN").map(entry("ready")),
48
+ ...check.missing.map(entry("install")),
49
+ ...check.missingPaid.map(entry("purchase")),
50
+ ...(check.unavailable ?? []).map(entry("unavailable"))
51
+ ];
52
+ }
53
+ /** `{{ $vars.API_URL }}` (expressions) and `${vars.API_URL}` (legacy). */
54
+ const VARIABLE_PATTERNS = [/\$vars\.([A-Za-z_][A-Za-z0-9_]*)/g, /\$\{vars\.([A-Za-z_][A-Za-z0-9_]*)\}/g];
55
+ /** The workspace values (`$vars`) a template's definition reads, sorted. */
56
+ function templateVariableNames(definition) {
57
+ let text;
58
+ try {
59
+ text = JSON.stringify(definition ?? null);
60
+ } catch {
61
+ return [];
62
+ }
63
+ const names = /* @__PURE__ */ new Set();
64
+ for (const pattern of VARIABLE_PATTERNS) for (const match of text.matchAll(pattern)) if (match[1]) names.add(match[1]);
65
+ return [...names].sort();
66
+ }
67
+ /** Canvas-only nodes that are not steps (they never run). */
68
+ const NOT_A_STEP = /* @__PURE__ */ new Set(["sticky-note"]);
69
+ /** The template's steps by name, in canvas order. */
70
+ function templateSteps(nodes) {
71
+ const steps = [];
72
+ for (const node of nodes) {
73
+ const type = typeof node.type === "string" ? node.type : "";
74
+ if (NOT_A_STEP.has(type)) continue;
75
+ const id = typeof node.id === "string" ? node.id : String(steps.length);
76
+ const label = node.data?.label;
77
+ steps.push({
78
+ id,
79
+ name: typeof label === "string" && label.trim() ? label.trim() : humanizeIdentifier(type.replace(/^store\//, "")) || id
80
+ });
81
+ }
82
+ return steps;
83
+ }
84
+
85
+ // src/modules/templates/components/TemplateNeedsPanel.tsx
86
+ /**
87
+ * "What you'll need" beside a template's preview (Navigation v2,
88
+ * BOFF-11848): before "Use this template", the apps it uses and whether each
89
+ * is ready, the accounts to connect, the values it will ask for, the
90
+ * workspace values it reads (with a way to add a missing one right here),
91
+ * and the price, author, step count and how often it was used.
92
+ */
93
+ /** Download and view counts, written for the reader's locale. */
94
+ const COUNT_FORMAT = new Intl.NumberFormat(void 0, {
95
+ notation: "compact",
96
+ maximumFractionDigits: 1
97
+ });
98
+ const APP_STATUS_ICON = {
99
+ ready: "mdi:check-circle-outline",
100
+ install: "mdi:download-outline",
101
+ purchase: "mdi:cart-outline",
102
+ unavailable: "mdi:alert-circle-outline"
103
+ };
104
+ function Section({ title, children, testId }) {
105
+ return /* @__PURE__ */ jsxs("section", {
106
+ className: "space-y-1.5",
107
+ "data-testid": testId,
108
+ children: [/* @__PURE__ */ jsx("h3", {
109
+ className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground",
110
+ children: title
111
+ }), children]
112
+ });
113
+ }
114
+ function AddWorkspaceValue({ name, onAdded }) {
115
+ const tr = useTr();
116
+ const { createVariable, loading } = useCreateVariable();
117
+ const [value, setValue] = useState("");
118
+ const save = async () => {
119
+ try {
120
+ await createVariable({
121
+ key: name,
122
+ value
123
+ });
124
+ toast.success(tr("fluidgrids.pages.templatePreview.addValueSaved", "Saved {{name}}", { name }));
125
+ onAdded();
126
+ } catch (err) {
127
+ const failed = tr("fluidgrids.pages.templatePreview.addValueFailed", "Could not save {{name}}", { name });
128
+ toast.error(failed, { description: describeOperationError(err, failed) });
129
+ }
130
+ };
131
+ return /* @__PURE__ */ jsxs("div", {
132
+ className: "flex items-center gap-2",
133
+ children: [/* @__PURE__ */ jsx(Input, {
134
+ value,
135
+ onChange: (event) => setValue(event.target.value),
136
+ placeholder: tr("fluidgrids.pages.templatePreview.addValuePlaceholder", "Value for {{name}}", { name }),
137
+ "aria-label": tr("fluidgrids.pages.templatePreview.addValuePlaceholder", "Value for {{name}}", { name }),
138
+ className: "h-8 text-xs",
139
+ "data-testid": `template-value-input-${name}`
140
+ }), /* @__PURE__ */ jsx(SdkPermissionButton, {
141
+ permission: FLUIDGRIDS_PERMISSIONS.VARIABLE_CREATE,
142
+ deniedBehavior: "hide",
143
+ children: /* @__PURE__ */ jsx(Button$1, {
144
+ "data-agent": "confirm",
145
+ size: "sm",
146
+ variant: "outline",
147
+ disabled: loading || !value.trim(),
148
+ onClick: () => void save(),
149
+ "data-testid": `template-value-save-${name}`,
150
+ children: tr("fluidgrids.pages.templatePreview.addValueSave", "Save")
151
+ })
152
+ })]
153
+ });
154
+ }
155
+ /** The `$vars` the template reads, checked against the workspace's values. */
156
+ function WorkspaceValuesCheck({ names }) {
157
+ const tr = useTr();
158
+ const { variables, loading, refetch } = useVariables();
159
+ const known = new Set(variables.map((variable) => variable.key));
160
+ const missing = loading ? [] : names.filter((name) => !known.has(name));
161
+ const missingSet = new Set(missing);
162
+ return /* @__PURE__ */ jsxs("div", {
163
+ className: "space-y-2",
164
+ children: [missing.length > 0 && /* @__PURE__ */ jsx("p", {
165
+ className: "text-xs text-text-primary",
166
+ "data-testid": "template-values-missing",
167
+ children: tr("fluidgrids.pages.templatePreview.needsValuesMissing", "This workflow needs: {{names}}", { names: missing.join(", ") })
168
+ }), /* @__PURE__ */ jsx("ul", {
169
+ className: "space-y-2",
170
+ children: names.map((name) => {
171
+ const isMissing = missingSet.has(name);
172
+ return /* @__PURE__ */ jsxs("li", {
173
+ className: "space-y-1 text-xs",
174
+ children: [/* @__PURE__ */ jsxs("div", {
175
+ className: "flex items-center justify-between gap-2",
176
+ children: [/* @__PURE__ */ jsx("span", {
177
+ className: "font-mono text-text-primary",
178
+ children: name
179
+ }), !loading && /* @__PURE__ */ jsx("span", {
180
+ className: isMissing ? "text-status-warning-text" : "text-text-secondary",
181
+ children: isMissing ? tr("fluidgrids.pages.templatePreview.valueMissing", "Missing") : tr("fluidgrids.pages.templatePreview.valueSet", "Set")
182
+ })]
183
+ }), isMissing && /* @__PURE__ */ jsx(AddWorkspaceValue, {
184
+ name,
185
+ onAdded: () => void refetch()
186
+ })]
187
+ }, name);
188
+ })
189
+ })]
190
+ });
191
+ }
192
+ function AppsSection({ nodeCheck }) {
193
+ const tr = useTr();
194
+ const apps = nodeCheck ? templateApps(nodeCheck) : [];
195
+ const statusText = {
196
+ ready: tr("fluidgrids.pages.templatePreview.appReady", "Ready"),
197
+ install: tr("fluidgrids.pages.templatePreview.appWillInstall", "Added for you when you use it"),
198
+ purchase: tr("fluidgrids.pages.templatePreview.appNeedsPurchase", "Needs to be bought first"),
199
+ unavailable: tr("fluidgrids.pages.templatePreview.appUnavailable", "Not in the store")
200
+ };
201
+ const empty = nodeCheck ? tr("fluidgrids.pages.templatePreview.needsNoApps", "Only built-in steps, nothing to add") : tr("fluidgrids.pages.templatePreview.needsChecking", "Checking…");
202
+ return /* @__PURE__ */ jsx(Section, {
203
+ title: tr("fluidgrids.pages.templatePreview.needsApps", "Apps it uses"),
204
+ testId: "template-needs-apps",
205
+ children: apps.length > 0 ? /* @__PURE__ */ jsx("ul", {
206
+ className: "space-y-1.5",
207
+ children: apps.map((app) => /* @__PURE__ */ jsxs("li", {
208
+ className: "flex items-start justify-between gap-2 text-xs",
209
+ "data-status": app.status,
210
+ children: [/* @__PURE__ */ jsx("span", {
211
+ className: "text-text-primary",
212
+ children: app.name
213
+ }), /* @__PURE__ */ jsxs("span", {
214
+ className: "flex shrink-0 items-center gap-1 text-text-secondary",
215
+ children: [/* @__PURE__ */ jsx(Icon, {
216
+ icon: APP_STATUS_ICON[app.status],
217
+ width: 14
218
+ }), statusText[app.status]]
219
+ })]
220
+ }, app.nodeType))
221
+ }) : /* @__PURE__ */ jsx("p", {
222
+ className: "text-xs text-text-secondary",
223
+ children: empty
224
+ })
225
+ });
226
+ }
227
+ /** A titled list of names, or a line saying there are none. */
228
+ function NamesSection({ title, names, empty, testId }) {
229
+ if (names.length === 0 && empty === void 0) return null;
230
+ return /* @__PURE__ */ jsx(Section, {
231
+ title,
232
+ testId,
233
+ children: names.length > 0 ? /* @__PURE__ */ jsx("ul", {
234
+ className: "space-y-1 text-xs text-text-primary",
235
+ children: names.map((name) => /* @__PURE__ */ jsx("li", { children: name }, name))
236
+ }) : /* @__PURE__ */ jsx("p", {
237
+ className: "text-xs text-text-secondary",
238
+ children: empty
239
+ })
240
+ });
241
+ }
242
+ function ValuesSection({ names }) {
243
+ const tr = useTr();
244
+ const { allowed: canReadValues } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.VARIABLE_READ);
245
+ if (names.length === 0) return null;
246
+ return /* @__PURE__ */ jsx(Section, {
247
+ title: tr("fluidgrids.pages.templatePreview.needsValues", "Workspace values"),
248
+ testId: "template-needs-values",
249
+ children: canReadValues ? /* @__PURE__ */ jsx(WorkspaceValuesCheck, { names }) : /* @__PURE__ */ jsx("p", {
250
+ className: "text-xs text-text-primary",
251
+ children: tr("fluidgrids.pages.templatePreview.needsValuesMissing", "This workflow needs: {{names}}", { names: names.join(", ") })
252
+ })
253
+ });
254
+ }
255
+ function FactsList({ template, stepCount }) {
256
+ const tr = useTr();
257
+ const author = template.authorName || template.publisher?.name || tr("fluidgrids.builder.templateCard.unknownAuthor", "Unknown");
258
+ const price = template.pricingModel === "FREE" ? tr("fluidgrids.builder.templateCard.freeBadge", "Free") : formatTemplatePrice(template.price, template.currency);
259
+ return /* @__PURE__ */ jsxs("dl", {
260
+ className: "grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 border-t border-card-foreground/10 pt-3 text-xs",
261
+ "data-testid": "template-needs-facts",
262
+ children: [
263
+ /* @__PURE__ */ jsx("dt", {
264
+ className: "text-text-secondary",
265
+ children: tr("fluidgrids.pages.templatePreview.factPrice", "Price")
266
+ }),
267
+ /* @__PURE__ */ jsx("dd", {
268
+ className: "text-text-primary",
269
+ children: price
270
+ }),
271
+ /* @__PURE__ */ jsx("dt", {
272
+ className: "text-text-secondary",
273
+ children: tr("fluidgrids.pages.templatePreview.factAuthor", "Made by")
274
+ }),
275
+ /* @__PURE__ */ jsxs("dd", {
276
+ className: "text-text-primary",
277
+ children: [author, template.publisher?.isVerified && /* @__PURE__ */ jsxs("span", {
278
+ className: "ml-1 text-text-secondary",
279
+ children: [
280
+ "(",
281
+ tr("fluidgrids.builder.templateCard.verifiedLabel", "Verified publisher"),
282
+ ")"
283
+ ]
284
+ })]
285
+ }),
286
+ /* @__PURE__ */ jsx("dt", {
287
+ className: "text-text-secondary",
288
+ children: tr("fluidgrids.pages.templatePreview.factSteps", "Steps")
289
+ }),
290
+ /* @__PURE__ */ jsx("dd", {
291
+ className: "text-text-primary",
292
+ children: stepCount
293
+ }),
294
+ /* @__PURE__ */ jsx("dt", {
295
+ className: "text-text-secondary",
296
+ children: tr("fluidgrids.pages.templatePreview.factUsage", "Used")
297
+ }),
298
+ /* @__PURE__ */ jsx("dd", {
299
+ className: "text-text-primary",
300
+ children: tr("fluidgrids.pages.templatePreview.factUsageValue", "{{downloads}} times · {{views}} views", {
301
+ downloads: COUNT_FORMAT.format(template.downloads ?? 0),
302
+ views: COUNT_FORMAT.format(template.viewCount ?? 0)
303
+ })
304
+ })
305
+ ]
306
+ });
307
+ }
308
+ function StepsList({ steps }) {
309
+ const tr = useTr();
310
+ if (steps.length === 0) return null;
311
+ return /* @__PURE__ */ jsx(Disclosure, {
312
+ closedLabel: tr("fluidgrids.pages.templatePreview.showSteps", "Show the {{count}} steps", { count: steps.length }),
313
+ openLabel: tr("fluidgrids.pages.templatePreview.hideSteps", "Hide the steps"),
314
+ "data-testid": "template-needs-steps",
315
+ children: /* @__PURE__ */ jsx("ol", {
316
+ className: "list-decimal space-y-0.5 pl-5 text-xs text-text-primary",
317
+ children: steps.map((step) => /* @__PURE__ */ jsx("li", { children: step.name }, step.id))
318
+ })
319
+ });
320
+ }
321
+ function TemplateNeedsPanel({ template, content, nodeCheck, steps }) {
322
+ const tr = useTr();
323
+ return /* @__PURE__ */ jsxs("aside", {
324
+ className: "flex max-h-[40vh] w-full shrink-0 flex-col gap-4 overflow-y-auto border-b border-card-foreground/10 bg-card p-4 lg:max-h-none lg:w-80 lg:border-b-0 lg:border-l",
325
+ "aria-labelledby": "template-needs-title",
326
+ "data-testid": "template-needs-panel",
327
+ children: [
328
+ /* @__PURE__ */ jsx("h2", {
329
+ id: "template-needs-title",
330
+ className: "text-sm font-semibold text-card-foreground",
331
+ children: tr("fluidgrids.pages.templatePreview.needsTitle", "What you'll need")
332
+ }),
333
+ /* @__PURE__ */ jsx(AppsSection, { nodeCheck }),
334
+ /* @__PURE__ */ jsx(NamesSection, {
335
+ title: tr("fluidgrids.pages.templatePreview.needsAccounts", "Accounts to connect"),
336
+ names: (content?.requiredCredentialTypes ?? []).map(humanizeIdentifier),
337
+ empty: tr("fluidgrids.pages.templatePreview.needsNoAccounts", "No accounts needed"),
338
+ testId: "template-needs-accounts"
339
+ }),
340
+ /* @__PURE__ */ jsx(NamesSection, {
341
+ title: tr("fluidgrids.pages.templatePreview.needsInputs", "You'll be asked for"),
342
+ names: (content?.requiredInputs ?? []).map((input) => input.label || input.name),
343
+ testId: "template-needs-inputs"
344
+ }),
345
+ /* @__PURE__ */ jsx(ValuesSection, { names: templateVariableNames(content?.definitionJson) }),
346
+ /* @__PURE__ */ jsx(FactsList, {
347
+ template,
348
+ stepCount: steps.length
349
+ }),
350
+ /* @__PURE__ */ jsx(StepsList, { steps })
351
+ ]
352
+ });
353
+ }
354
+
355
+ // src/utils/template-graph.ts
356
+ /**
357
+ * Extract nodes/edges from a template `content` object.
358
+ * Returns null when the content carries no renderable graph.
359
+ */
360
+ function extractTemplateGraph(content) {
361
+ const dj = content?.definitionJson;
362
+ if (!dj) return null;
363
+ const graph = dj.data ?? dj;
364
+ const nodes = graph?.nodes ?? [];
365
+ const edges = graph?.edges ?? [];
366
+ if (!nodes.length) return null;
367
+ return {
368
+ nodes,
369
+ edges
370
+ };
371
+ }
372
+
373
+ // src/pages/useTemplatePreviewLoad.ts
374
+ /**
375
+ * Loads a store template for the preview page: the product, its content and
376
+ * the graph the read-only canvas draws, or a plain error. With Navigation v2
377
+ * (BOFF-11848) it also works out the apps the template needs, for the "What
378
+ * you'll need" panel: the template's own list when it has one, else from its
379
+ * nodes (as "Use this template" does). The canvas does not wait for that.
380
+ */
381
+ function useTemplatePreviewLoad(productId, withRequiredNodes) {
382
+ const tr = useTr();
383
+ const { getTemplateDetail, parseTemplateContent } = useImportWorkflowTemplate();
384
+ const [workflow, setWorkflow] = useState(null);
385
+ const [template, setTemplate] = useState(null);
386
+ const [content, setContent] = useState(null);
387
+ const [loading, setLoading] = useState(true);
388
+ const [error, setError] = useState(null);
389
+ const [requiredNodes, setRequiredNodes] = useState(null);
390
+ useEffect(() => {
391
+ let mounted = true;
392
+ const fail = (message) => {
393
+ if (!mounted) return;
394
+ setError(message);
395
+ setLoading(false);
396
+ };
397
+ const loadFailed = () => tr("fluidgrids.pages.templatePreview.errorLoadFailed", "Failed to load template.");
398
+ const load = async () => {
399
+ if (!productId) {
400
+ fail(tr("fluidgrids.pages.templatePreview.errorNoTemplateSpecified", "No template specified."));
401
+ return;
402
+ }
403
+ try {
404
+ const full = await getTemplateDetail(productId);
405
+ if (!full) {
406
+ fail(loadFailed());
407
+ return;
408
+ }
409
+ const parsed = parseTemplateContent(full);
410
+ const graph = extractTemplateGraph(parsed);
411
+ const nodes = graph?.nodes ?? [];
412
+ const edges = graph?.edges ?? [];
413
+ if (!nodes.length) {
414
+ fail(tr("fluidgrids.pages.templatePreview.errorNoWorkflow", "This template has no workflow to preview."));
415
+ return;
416
+ }
417
+ if (mounted) {
418
+ setTemplate(full);
419
+ setContent(parsed);
420
+ setWorkflow({
421
+ id: productId,
422
+ workflowKey: productId,
423
+ version: "preview",
424
+ name: full.name ?? tr("fluidgrids.pages.templatePreview.fallbackTemplateName", "Template"),
425
+ nodes,
426
+ edges,
427
+ sidebar: {
428
+ active: "none",
429
+ panels: { nodeProperties: { selectedNode: null } }
430
+ }
431
+ });
432
+ setLoading(false);
433
+ }
434
+ if (withRequiredNodes) {
435
+ const listed = parsed?.requiredNodeTypes ?? [];
436
+ const required = listed.length ? listed : await buildRequiredNodeTypes(nodes).catch(() => []);
437
+ if (mounted) setRequiredNodes(required);
438
+ }
439
+ } catch (e) {
440
+ fail(e instanceof Error ? e.message : loadFailed());
441
+ }
442
+ };
443
+ load();
444
+ return () => {
445
+ mounted = false;
446
+ };
447
+ }, [
448
+ productId,
449
+ getTemplateDetail,
450
+ parseTemplateContent,
451
+ tr,
452
+ withRequiredNodes
453
+ ]);
454
+ return {
455
+ workflow,
456
+ template,
457
+ content,
458
+ loading,
459
+ error,
460
+ requiredNodes
461
+ };
462
+ }
463
+
464
+ // src/pages/TemplatePreview.tsx
465
+ var TemplatePreview_exports = /* @__PURE__ */ __exportAll({ TemplatePreview: () => TemplatePreview });
466
+ /**
467
+ * TemplatePreview — a read-only, n8n-style preview of a store workflow template.
468
+ *
469
+ * It loads the template's `content.definitionJson`, seeds the builder's flow
470
+ * store with the graph, and renders the *real* builder canvas in read-only mode
471
+ * (no dragging/connecting/editing — pan + zoom only). A header shows the
472
+ * template name/description and a "Use this template" CTA that imports the
473
+ * template and opens the editable builder. Nothing is created until the user
474
+ * clicks "Use this template".
475
+ *
476
+ * Navigation v2 (BOFF-11848, FluidGrids only): a "What you'll need" panel
477
+ * beside the canvas (apps and their status, accounts, values, price, author,
478
+ * steps, use counts), the canvas fits the whole template on screen, the badge
479
+ * says nothing is created until Use, and a paid template asks first.
480
+ */
481
+ /** The read-only badge: today's words, or (v2) why it matters. */
482
+ function PreviewBadge({ plain }) {
483
+ const text = useTr()("fluidgrids.pages.templatePreview.readOnlyBadge", plain ? "Preview — nothing is created until you click Use" : "Preview · read-only");
484
+ return /* @__PURE__ */ jsx("span", {
485
+ className: plain ? "min-w-0 truncate rounded-full bg-card-foreground/10 px-2 py-0.5 text-[11px] font-medium text-muted-foreground" : "shrink-0 rounded-full bg-card-foreground/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground",
486
+ title: plain ? text : void 0,
487
+ "data-testid": "template-preview-badge",
488
+ children: text
489
+ });
490
+ }
491
+ /** A message in place of the canvas, with the way back to Templates. */
492
+ function PreviewMessage({ message, onBack, icon }) {
493
+ const tr = useTr();
494
+ return /* @__PURE__ */ jsxs("div", {
495
+ className: "flex h-full flex-col items-center justify-center gap-3 p-8 text-center",
496
+ children: [
497
+ icon && /* @__PURE__ */ jsx(Icon, {
498
+ icon,
499
+ className: "size-8 text-muted-foreground"
500
+ }),
501
+ /* @__PURE__ */ jsx("p", {
502
+ className: "text-sm text-muted-foreground",
503
+ children: message
504
+ }),
505
+ /* @__PURE__ */ jsx(Button$1, {
506
+ variant: "outline",
507
+ onClick: onBack,
508
+ children: tr("fluidgrids.pages.templatePreview.backToTemplates", "Back to templates")
509
+ })
510
+ ]
511
+ });
512
+ }
513
+ function PreviewLoading() {
514
+ const tr = useTr();
515
+ return /* @__PURE__ */ jsxs("div", {
516
+ className: "flex h-full items-center justify-center gap-2 text-sm text-muted-foreground",
517
+ children: [/* @__PURE__ */ jsx(Icon, {
518
+ icon: "svg-spinners:ring-resize",
519
+ className: "size-4"
520
+ }), tr("fluidgrids.pages.templatePreview.loadingPreview", "Loading preview…")]
521
+ });
522
+ }
523
+ function TemplatePreview() {
524
+ const { productId } = useParams();
525
+ const navigate = useNavigate();
526
+ const location = useLocation();
527
+ const tr = useTr();
528
+ const v2 = useNavigationV2();
529
+ const { installedAppIds } = useInstalledNodes();
530
+ const { workflow, template, content, loading, error, requiredNodes } = useTemplatePreviewLoad(productId, v2);
531
+ const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);
532
+ const [confirmPaid, setConfirmPaid] = useState(false);
533
+ const workflowsBase = location.pathname.replace(/\/templates(\/.*)?$/, "");
534
+ const nodeCheck = useMemo(() => requiredNodes ? buildNodeCheck(requiredNodes, installedAppIds) : null, [requiredNodes, installedAppIds]);
535
+ const steps = useMemo(() => templateSteps(workflow?.nodes ?? []), [workflow]);
536
+ const templateImport = useTemplateImportFlow({
537
+ onImported: (created) => navigate(`${workflowsBase}/workflow/${created.workflowKey}/${created.version}`),
538
+ onQuotaExhausted: () => setShowQuotaExhausted(true)
539
+ });
540
+ const importing = templateImport.busy;
541
+ const handleUse = () => {
542
+ if (!template) return;
543
+ if (v2 && isPaidTemplate(template)) {
544
+ setConfirmPaid(true);
545
+ return;
546
+ }
547
+ templateImport.start(template);
548
+ };
549
+ const backToTemplates = () => navigate(`${workflowsBase}/templates`);
550
+ return /* @__PURE__ */ jsxs(ThemeProvider, { children: [
551
+ /* @__PURE__ */ jsxs("div", {
552
+ className: "flex h-full w-full flex-col overflow-hidden",
553
+ children: [/* @__PURE__ */ jsxs("div", {
554
+ className: "flex h-14 shrink-0 items-center justify-between gap-3 border-b border-card-foreground/10 bg-gradient-to-r from-primary/40 to-transparent px-3",
555
+ children: [/* @__PURE__ */ jsxs("div", {
556
+ className: "flex min-w-0 items-center gap-2",
557
+ children: [/* @__PURE__ */ jsx("button", {
558
+ type: "button",
559
+ onClick: backToTemplates,
560
+ title: tr("fluidgrids.pages.templatePreview.backTooltip", "Back to templates"),
561
+ className: "flex size-9 shrink-0 items-center justify-center rounded-md text-card-foreground transition-colors hover:bg-card-foreground/10",
562
+ children: /* @__PURE__ */ jsx(Icon, {
563
+ icon: "ion:arrow-back-outline",
564
+ className: "size-5"
565
+ })
566
+ }), /* @__PURE__ */ jsxs("div", {
567
+ className: "min-w-0",
568
+ children: [/* @__PURE__ */ jsxs("div", {
569
+ className: "flex items-center gap-2",
570
+ children: [/* @__PURE__ */ jsx("span", {
571
+ className: "truncate text-sm font-semibold text-card-foreground",
572
+ children: workflow?.name ?? template?.name ?? tr("fluidgrids.pages.templatePreview.fallbackHeaderTitle", "Template preview")
573
+ }), /* @__PURE__ */ jsx(PreviewBadge, { plain: v2 })]
574
+ }), template?.description ? /* @__PURE__ */ jsx("span", {
575
+ className: "block truncate text-xs text-muted-foreground",
576
+ children: template.description
577
+ }) : null]
578
+ })]
579
+ }), /* @__PURE__ */ jsx(WithOpenTemplateAsCode, {
580
+ templateName: template ? template.name : null,
581
+ content,
582
+ className: "shrink-0",
583
+ wrapperClassName: "flex shrink-0 items-center gap-2",
584
+ children: /* @__PURE__ */ jsx(SdkPermissionButton, {
585
+ permission: FLUIDGRIDS_PERMISSIONS.WORKFLOW_CREATE,
586
+ deniedTooltip: tr("fluidgrids.templates.useDeniedTooltip", "You don't have permission to create workflows from templates"),
587
+ children: /* @__PURE__ */ jsxs(Button$1, {
588
+ onClick: handleUse,
589
+ disabled: importing || !template,
590
+ className: "shrink-0 gap-2",
591
+ children: [/* @__PURE__ */ jsx(Icon, {
592
+ icon: importing ? "svg-spinners:ring-resize" : "mdi:plus",
593
+ className: "size-4"
594
+ }), tr("fluidgrids.pages.templatePreview.useThisTemplate", "Use this template")]
595
+ })
596
+ })
597
+ })]
598
+ }), /* @__PURE__ */ jsxs("div", {
599
+ className: v2 ? "flex min-h-0 w-full flex-1 flex-col lg:flex-row-reverse" : "contents",
600
+ children: [v2 && template && workflow && /* @__PURE__ */ jsx(TemplateNeedsPanel, {
601
+ template,
602
+ content,
603
+ nodeCheck,
604
+ steps
605
+ }), /* @__PURE__ */ jsx("div", {
606
+ className: "relative min-h-0 w-full flex-1",
607
+ children: /* @__PURE__ */ jsx(ErrorBoundary, {
608
+ fallback: () => /* @__PURE__ */ jsx(PreviewMessage, {
609
+ message: tr("fluidgrids.pages.templatePreview.renderErrorMessage", "Something went wrong rendering this template preview."),
610
+ onBack: backToTemplates
611
+ }),
612
+ children: loading ? /* @__PURE__ */ jsx(PreviewLoading, {}) : error ? /* @__PURE__ */ jsx(PreviewMessage, {
613
+ icon: "mdi:alert-circle-outline",
614
+ message: error,
615
+ onBack: backToTemplates
616
+ }) : workflow ? /* @__PURE__ */ jsx(FlowBuilderPage, {
617
+ workflow,
618
+ readOnly: true,
619
+ fitToScreen: v2
620
+ }) : null
621
+ })
622
+ })]
623
+ })]
624
+ }),
625
+ templateImport.dialog,
626
+ /* @__PURE__ */ jsx(PaidTemplateConfirm, {
627
+ template: confirmPaid ? template : null,
628
+ onConfirm: () => {
629
+ setConfirmPaid(false);
630
+ if (template) templateImport.start(template);
631
+ },
632
+ onCancel: () => setConfirmPaid(false)
633
+ }),
634
+ /* @__PURE__ */ jsx(QuotaExhaustedDialog, {
635
+ surface: "template-import",
636
+ open: showQuotaExhausted,
637
+ onOpenChange: setShowQuotaExhausted,
638
+ onUpgrade: () => {
639
+ setShowQuotaExhausted(false);
640
+ navigate(BILLING_PLANS_PATH);
641
+ }
642
+ })
643
+ ] });
644
+ }
645
+
646
+ export { TemplatePreview_exports as n, extractTemplateGraph as r, TemplatePreview as t };