@burdenoff/fluidgrids-fe-sdk 2026.1111.16 → 2026.1111.18

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 (119) hide show
  1. package/README.md +41 -0
  2. package/dist/AiStudioPage-BHVEU4Wp.js +774 -0
  3. package/dist/ApprovalInboxPage-dbBDSJ-f.js +558 -0
  4. package/dist/DataSinkDashboardPage-1h2QsMzH.js +270 -0
  5. package/dist/DataTablesPage-BZy2z0LB.js +1690 -0
  6. package/dist/EntityTagPicker-BqRNsLCQ.js +135 -0
  7. package/dist/{ExecutionDetailPage-D-0_POAf.js → ExecutionDetailPage-BdcZmLT4.js} +799 -4420
  8. package/dist/ExecutionDetailPage-DvQ667OE.d.ts +10 -0
  9. package/dist/ExecutionsListPage-BCUJoAIE.js +2742 -0
  10. package/dist/{FailureTriagePanel-BHiduL7A.js → FailureTriagePanel-B9hDQymi.js} +4 -97
  11. package/dist/{FluidGridsProvider-DmvFADFa.js → FluidGridsProvider-dvZSNtSW.js} +10 -350
  12. package/dist/HistoryPage-BLoRpJ1R.d.ts +6 -0
  13. package/dist/{HistoryPage-BbUgxx1I.js → HistoryPage-U0tqupBN.js} +6 -4
  14. package/dist/HumanRequestCard-D-ooVPDn.js +439 -0
  15. package/dist/{InsightsPage-s6f5bU-8.js → InsightsPage-CALo16eB.js} +17 -328
  16. package/dist/ManagedAiPolicyAuthority-BxwJ3ptd.js +40 -0
  17. package/dist/{ManagedAiPolicyAuthority-CGux_DaV.d.ts → ManagedAiPolicyAuthority-DM3t3wNr.d.ts} +1 -5
  18. package/dist/ManagedAiPolicyAuthorityContext-CNkxIOZ1.js +10 -0
  19. package/dist/{MissingNodesInstallModal-B-9gHy2K.js → MissingNodesInstallModal-CMybsI8h.js} +3 -2
  20. package/dist/{NodesPage-ClvhUfbh.js → NodesPage-Bf5PhfBK.js} +7 -3
  21. package/dist/NodesPage-DEbnz9iU.d.ts +6 -0
  22. package/dist/{PublishCustomNodeAction-D2F7oN0S.js → PublishCustomNodeAction-5XXnSA3H.js} +3 -45
  23. package/dist/QuotaExhaustedDialog-7IFSaLyi.js +54 -0
  24. package/dist/RunnerStatusPill-Ctg4TJcP.js +187 -0
  25. package/dist/RunnersPage-CLCOCclc.js +467 -0
  26. package/dist/{ManagedAiPolicyAuthority-DsSaz-vg.js → SettingsPage-CJLZvv4J.js} +743 -773
  27. package/dist/SettingsPage-DMHQFj5-.d.ts +9 -0
  28. package/dist/TagFilterSelect-DCBOH6Ou.js +41 -0
  29. package/dist/TaskQueuesPage-9VgSnLdL.js +951 -0
  30. package/dist/TemplateCard-DsC0KNH1.js +1416 -0
  31. package/dist/{TemplateConfigurationModal-D_Mvgxqa.js → TemplateConfigurationModal-BhGWIzpm.js} +3 -2
  32. package/dist/{TemplateDetailPanel-DfFfUcOe.js → TemplateDetailPanel-D4cUVr-m.js} +3 -3
  33. package/dist/TemplatePreview-FayF6jxh.js +231 -0
  34. package/dist/TemplatesPage-BG3H-6v6.d.ts +10 -0
  35. package/dist/{TemplatesPage-Cku13bav.js → TemplatesPage-CsDNT54N.js} +11 -228
  36. package/dist/VariablesPage-67WR_lVE.js +949 -0
  37. package/dist/{VersionDiffDialog-Dq2xlPFI.js → VersionDiffDialog-DSN0mU0f.js} +2 -1
  38. package/dist/WorkflowBuilder-CPnXEL5J.js +555 -0
  39. package/dist/{WorkflowSearchRedirect-CaoOOpEl.js → WorkflowBuilder-YXRysvZc.js} +19 -585
  40. package/dist/{WorkflowProposalCard-8Vr56kQa.js → WorkflowProposalCard-qrEeNyYG.js} +4 -405
  41. package/dist/WorkflowSearchRedirect-DwDNZ_q5.js +36 -0
  42. package/dist/WorkflowShareDialog-mbIwRQUY.js +168 -0
  43. package/dist/WorkflowsList-CcNDQge5.d.ts +6 -0
  44. package/dist/{WorkflowsList-B2j1Pehb.js → WorkflowsList-DiiGyZkT.js} +12 -6
  45. package/dist/agent-form-Bfvm32j1.js +83 -0
  46. package/dist/{agentTools-DJ5o2ekg.js → agentTools-8s_25Q_F.js} +3 -2
  47. package/dist/{ai-usage-D5p7ZeRR.js → ai-usage-glCwHyQz.js} +1 -1
  48. package/dist/api-BrCHB5oA.js +224 -0
  49. package/dist/api-C8e0iV-Q.js +55 -0
  50. package/dist/button-DG7y_FVJ.js +48 -0
  51. package/dist/{canvasNoteTools-D3zUdEeg.js → canvasNoteTools-B6pYARgU.js} +2 -2
  52. package/dist/client-C9XSzSXr.js +1147 -0
  53. package/dist/code-editor-field-9-GdVE_8.js +111 -0
  54. package/dist/{codec-client-DstqyzTN.js → codec-client-CJ95Rmqo.js} +1 -1
  55. package/dist/constants-B2lhWNoN.js +202 -0
  56. package/dist/constants-XdvUxeWK.d.ts +162 -0
  57. package/dist/dialog-Cjb4lB4D.js +105 -0
  58. package/dist/executions.d.ts +1 -1
  59. package/dist/executions.js +2 -1
  60. package/dist/feature-flags-DM5myWaD.js +296 -0
  61. package/dist/flags.d.ts +3 -0
  62. package/dist/flags.js +4 -0
  63. package/dist/{flow-builder-DEzT8TQu.js → flow-builder-CZDKRc6f.js} +662 -10716
  64. package/dist/{node-endpoint-configs-DHFGA5MQ.js → graphql-operation-types-kX-MMjgW.js} +1 -1194
  65. package/dist/{graphql-types-XcDLtFwU.js → graphql-types-CDKVWH2R.js} +2 -202
  66. package/dist/history.d.ts +1 -1
  67. package/dist/history.js +1 -1
  68. package/dist/{index-DRf25-D3.d.ts → index-6JR2vD9J.d.ts} +33 -215
  69. package/dist/index-72XFgvbn.d.ts +132 -0
  70. package/dist/index-BcdtKBM-.d.ts +56 -0
  71. package/dist/{index-B3BWqafN.d.ts → index-DjiFgEQl.d.ts} +5 -131
  72. package/dist/index.d.ts +13 -9
  73. package/dist/index.js +52 -5300
  74. package/dist/{node-detail-view-LNqeIqN-.js → node-detail-view-CV4ZRwqh.js} +13 -8
  75. package/dist/node-editor-tab-body-DU-dh35X.js +174 -0
  76. package/dist/node-endpoint-configs-B5bjNMDW.js +37 -0
  77. package/dist/node-registry-refresh-Dy0uRg9f.js +84 -0
  78. package/dist/nodes.d.ts +1 -1
  79. package/dist/nodes.js +1 -1
  80. package/dist/observability.d.ts +1 -1
  81. package/dist/observability.js +2 -1
  82. package/dist/popover-BJHvrFGR.js +472 -0
  83. package/dist/{quick-config-panel-D3ogsaFL.js → quick-config-panel-ws3pLvbt.js} +3 -2
  84. package/dist/rolldown-runtime-hDbaFkMy.js +18 -0
  85. package/dist/run-display-BycKYSU0.js +46 -0
  86. package/dist/run-list-links-ByYC0OlV.js +98 -0
  87. package/dist/schedule-panel-C0mfGQSD.js +7542 -0
  88. package/dist/settings.d.ts +2 -1
  89. package/dist/settings.js +2 -1
  90. package/dist/shared-CMfn30xT.js +196 -0
  91. package/dist/sidebar-node-editor-CR1PiZO-.js +177 -0
  92. package/dist/templates.d.ts +1 -1
  93. package/dist/templates.js +2 -1
  94. package/dist/theme-provider-B3mIuz2d.js +11 -0
  95. package/dist/{useAgentDefinitions-NFtLytDR.js → useAgentDefinitions-B0dam3_o.js} +3 -397
  96. package/dist/useConnections-Cs7LCWEM.js +401 -0
  97. package/dist/useDurableParity-CzUQMFJ3.js +415 -0
  98. package/dist/useEntityTags-DhnsZcTl.js +236 -0
  99. package/dist/{useExecutions-DAzeWHS6.js → useExecutions-9aLuOt1c.js} +4 -3
  100. package/dist/{useObservability-COCZ0hUS.js → useObservability-CfEw3ZFD.js} +2 -2
  101. package/dist/{useRunHistory-CsYUbouO.js → useRunHistory-B1xJDwl1.js} +4 -3
  102. package/dist/{WorkflowShareDialog-6RRDe-_v.js → useWorkflowCodeExport-Bu2QYJzA.js} +7 -210
  103. package/dist/{useWorkflows-w4Dx2fF2.js → useWorkflows-BlVVu8NM.js} +3 -2
  104. package/dist/utils-uaL5UVxb.js +65 -0
  105. package/dist/workflow-builder.d.ts +1 -1
  106. package/dist/workflow-builder.js +4 -2
  107. package/dist/workflow-host.d.ts +6 -0
  108. package/dist/workflow-host.js +9 -0
  109. package/dist/workflow-pages.d.ts +55 -0
  110. package/dist/workflow-pages.js +113 -0
  111. package/dist/workflows-list.d.ts +1 -1
  112. package/dist/workflows-list.js +1 -1
  113. package/package.json +13 -1
  114. package/dist/ExecutionDetailPage-MvsnOnE1.d.ts +0 -4
  115. package/dist/HistoryPage-BqUizaFv.d.ts +0 -3
  116. package/dist/NodesPage-C97-9K2Y.d.ts +0 -3
  117. package/dist/TemplatesPage-D0qnXzHh.d.ts +0 -4
  118. package/dist/WorkflowsList-0d5GSKdh.d.ts +0 -3
  119. package/dist/agent-form-BvMWhQ42.js +0 -496
@@ -0,0 +1,1416 @@
1
+ "use client";
2
+ import { _ as throwIfResultError, n as getFluidGridsClient, x as getFluidGridsConfig } from "./client-C9XSzSXr.js";
3
+ import { i as getGlobalStoreClient } from "./node-registry-refresh-Dy0uRg9f.js";
4
+ import { A as StoreNodeProductsByTypeDocument, C as GetMyNodeInstallationsDocument, D as InstallWorkflowNodeDocument, c as ensureInstalledNodeTypesLoaded, d as ensureRegistryNodeTypesLoaded, i as AVAILABLE_NODES, o as NODE_CONFIGS, pn as useFluidGridsAnalytics, t as FluidGridsContext, x as BrowseWorkflowTemplatesDocument } from "./FluidGridsProvider-dvZSNtSW.js";
5
+ import { r as useTr } from "./TranslationBridge-Da-TPgQm.js";
6
+ import { a as emitTemplateUsed, s as emitWorkflowCreated, t as emitNodeInstalled, y as isQuotaExhaustedFailure } from "./fluidgridsEvents-CNdcrUNN.js";
7
+ import { t as cn } from "./cn-QqidFIgr.js";
8
+ import { t as Button } from "./button-DG7y_FVJ.js";
9
+ import { i as SdkPermissionButton, t as FLUIDGRIDS_PERMISSIONS } from "./constants-B2lhWNoN.js";
10
+ import React, { Suspense, forwardRef, lazy, useCallback, useContext, useMemo, useState } from "react";
11
+ import { useQuery } from "@apollo/client/react";
12
+ import { gql } from "@apollo/client/core";
13
+ import { Icon } from "@iconify/react";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ import { toast } from "sonner";
16
+ import { useEventBus } from "@burdenoff/fe-libs/shared/events";
17
+ import { cva } from "class-variance-authority";
18
+ import { Download, Eye, Sparkles, Star } from "lucide-react";
19
+
20
+ // src/feature-flags/durable-nodes.ts
21
+ /**
22
+ * Palette gating for the durable-function nodes (BOFF-9905).
23
+ *
24
+ * wspace-workflow-svc only seeds the `internal:code-workflow` and
25
+ * `internal:wait-for-signal` manifests while `DURABLE_NODE_MANIFESTS_ENABLED`
26
+ * is set, and refuses durable work while the `fluidgrids.durable-functions`
27
+ * flag is off for the workspace. Once the manifests are seeded for every
28
+ * workspace, the builder still has to hide the two nodes from the palettes of
29
+ * workspaces where the flag is off. This module is that client-side filter; it
30
+ * mirrors the server-side gate and is never the gate itself.
31
+ *
32
+ * Hiding only affects what can be picked from a palette. A workflow that
33
+ * already contains one of these nodes keeps rendering and running unchanged.
34
+ */
35
+ /** Node types offered only while durable functions are enabled. */
36
+ const DURABLE_FUNCTION_NODE_TYPES = /* @__PURE__ */ new Set(["internal:code-workflow", "internal:wait-for-signal"]);
37
+ /**
38
+ * The run-control nodes of Phase C (BOFF-9906, DC15). wspace-workflow-svc
39
+ * seeds their manifests only while `RUN_CONTROL_NODE_MANIFESTS_ENABLED` is
40
+ * set, which is turned on after this filter ships; the builder then still
41
+ * hides each one where its flag is off: Signal Run needs run control, Set
42
+ * Search Attributes needs search attributes.
43
+ */
44
+ const RUN_CONTROL_NODE_TYPES = /* @__PURE__ */ new Set(["internal:signal-run", "internal:set-search-attributes"]);
45
+ /**
46
+ * The Assert node of the workflow test suites (BOFF-9960). wspace-workflow-svc
47
+ * seeds its manifest for every workspace, so the builder hides it wherever the
48
+ * `fluidgrids.workflow-tests` flag is off.
49
+ */
50
+ const WORKFLOW_TEST_NODE_TYPES = /* @__PURE__ */ new Set(["internal:assert"]);
51
+ /** Whether a workflow node type is one of the Phase C run-control nodes. */
52
+ function isRunControlNodeType(nodeType) {
53
+ return !!nodeType && RUN_CONTROL_NODE_TYPES.has(nodeType);
54
+ }
55
+ /** Normalises the boolean (Phase B) or object form of the palette gates. */
56
+ function paletteGatesOf(gates) {
57
+ if (typeof gates === "boolean") return {
58
+ durableFunctions: gates,
59
+ runControl: false,
60
+ searchAttributes: false,
61
+ workflowTests: false
62
+ };
63
+ return {
64
+ durableFunctions: gates?.durableFunctions === true,
65
+ runControl: gates?.runControl === true,
66
+ searchAttributes: gates?.searchAttributes === true,
67
+ workflowTests: gates?.workflowTests === true
68
+ };
69
+ }
70
+ function hiddenBy(nodeType, gates) {
71
+ if (DURABLE_FUNCTION_NODE_TYPES.has(nodeType)) return !gates.durableFunctions;
72
+ if (nodeType === "internal:signal-run") return !gates.runControl;
73
+ if (nodeType === "internal:set-search-attributes") return !gates.searchAttributes;
74
+ if (WORKFLOW_TEST_NODE_TYPES.has(nodeType)) return !gates.workflowTests;
75
+ return false;
76
+ }
77
+ /** Whether a workflow node type is one of the durable-function nodes. */
78
+ function isDurableFunctionNodeType(nodeType) {
79
+ return !!nodeType && DURABLE_FUNCTION_NODE_TYPES.has(nodeType);
80
+ }
81
+ /**
82
+ * Whether a node type must be left out of the builder palettes. Fails closed:
83
+ * a family the caller does not say is enabled is not.
84
+ */
85
+ function isNodeTypeHiddenFromPalette(nodeType, gates = false) {
86
+ return !!nodeType && hiddenBy(nodeType, paletteGatesOf(gates));
87
+ }
88
+ /**
89
+ * Drops the palette entries that must stay hidden. Returns the input array
90
+ * itself when nothing is hidden, so the canvas palettes keep the exact list
91
+ * (and references) they had before this filter existed.
92
+ */
93
+ function filterPaletteNodes(nodes, gates = false) {
94
+ const resolved = paletteGatesOf(gates);
95
+ if (!nodes.some((node) => hiddenBy(node.type, resolved))) return nodes;
96
+ return nodes.filter((node) => !hiddenBy(node.type, resolved));
97
+ }
98
+
99
+ // src/ui/card.tsx
100
+ const Card = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
101
+ ref,
102
+ className: cn("rounded-lg border bg-card text-card-foreground shadow-sm transition-all duration-200 ease-out hover:shadow-md", className),
103
+ ...props
104
+ }));
105
+ Card.displayName = "Card";
106
+ const CardHeader = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
107
+ ref,
108
+ className: cn("flex flex-col space-y-1.5 p-6", className),
109
+ ...props
110
+ }));
111
+ CardHeader.displayName = "CardHeader";
112
+ const CardTitle = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("h3", {
113
+ ref,
114
+ className: cn("text-2xl font-semibold leading-none tracking-tight", className),
115
+ ...props
116
+ }));
117
+ CardTitle.displayName = "CardTitle";
118
+ const CardDescription = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("p", {
119
+ ref,
120
+ className: cn("text-sm text-muted-foreground", className),
121
+ ...props
122
+ }));
123
+ CardDescription.displayName = "CardDescription";
124
+ const CardContent = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
125
+ ref,
126
+ className: cn("p-6 pt-0", className),
127
+ ...props
128
+ }));
129
+ CardContent.displayName = "CardContent";
130
+ const CardFooter = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
131
+ ref,
132
+ className: cn("flex items-center p-6 pt-0", className),
133
+ ...props
134
+ }));
135
+ CardFooter.displayName = "CardFooter";
136
+
137
+ // src/hooks/store/useGlobalStoreClient.ts
138
+ /**
139
+ * Hook to get the Global Store Apollo client
140
+ *
141
+ * Priority:
142
+ * 1. FluidGridsProvider context (if available)
143
+ * 2. Module-level singleton set via setGlobalStoreClient()
144
+ *
145
+ * @throws Error if no global client is available from either source
146
+ */
147
+ function useGlobalStoreClient() {
148
+ const context = useContext(FluidGridsContext);
149
+ if (context?.globalClient) return context.globalClient;
150
+ const singletonClient = getGlobalStoreClient();
151
+ if (singletonClient) return singletonClient;
152
+ throw new Error("useGlobalStoreClient: No global client available. Either wrap your component tree with <FluidGridsProvider globalClient={...}> or call setGlobalStoreClient(client) before rendering.");
153
+ }
154
+
155
+ // src/hooks/store/useInstalledNodes.ts
156
+ /**
157
+ * Hook to get installed nodes for the current workspace
158
+ *
159
+ * Fetches installations from the Store service to show "Already Installed" status
160
+ * in the Store popup and display installed count.
161
+ */
162
+ function useInstalledNodes() {
163
+ const client = useGlobalStoreClient();
164
+ const workspaceId = useContext(FluidGridsContext)?.workspaceId ?? getFluidGridsConfig().workspaceId;
165
+ const { data, loading, error, refetch } = useQuery(GetMyNodeInstallationsDocument, {
166
+ client,
167
+ variables: {
168
+ workspaceId: workspaceId || "",
169
+ limit: 100,
170
+ offset: 0
171
+ },
172
+ skip: !workspaceId,
173
+ fetchPolicy: "cache-and-network"
174
+ });
175
+ const installations = data?.myStoreAppInstallations?.items || [];
176
+ const activeInstallations = installations.filter((item) => item.status === "ACTIVE");
177
+ return {
178
+ installations,
179
+ installedAppIds: new Set(activeInstallations.map((item) => item.productId)),
180
+ installationsMap: new Map(activeInstallations.map((item) => [item.productId, {
181
+ installedVersion: item.version ?? void 0,
182
+ installationId: item.id,
183
+ installedAt: item.installedAt
184
+ }])),
185
+ deprecatedInstallations: activeInstallations.filter((item) => item.productStatus === "UNLISTED"),
186
+ installedNodeTypes: /* @__PURE__ */ new Set(),
187
+ totalCount: data?.myStoreAppInstallations?.total || 0,
188
+ loading,
189
+ error,
190
+ refetch
191
+ };
192
+ }
193
+
194
+ // src/builder/components/flow-builder/components/store/store-node-availability.ts
195
+ /**
196
+ * Operations a user may actually pick for a node type: adapter operations
197
+ * (supplyModel/supplyTool/…) and `internal` ones (the AI Agent's planStep) are
198
+ * executor plumbing, chosen automatically, and must not count as user choices.
199
+ */
200
+ function userSelectableOperationCount(nodeType) {
201
+ if (!nodeType) return 0;
202
+ return (NODE_CONFIGS.find((c) => c.type === nodeType)?.operations ?? []).filter((op) => !op.isAdapterOperation && !op.internal).length;
203
+ }
204
+ /**
205
+ * Resolve the workflow node type for a store node.
206
+ *
207
+ * Installed nodes can fall back to the historical slug-based type when older
208
+ * manifests did not yet include `manifest.nodeType`.
209
+ */
210
+ function getStoreNodeType(node, isInstalled) {
211
+ return node.manifest?.nodeType ?? (isInstalled ? `store/${node.slug}` : void 0);
212
+ }
213
+ /**
214
+ * A node is considered prebuilt when it is already present in the workflow
215
+ * registry/available nodes list even though the workspace does not have a store
216
+ * installation record for it.
217
+ */
218
+ function getStoreNodeAvailability(node, isInstalled, options = {}) {
219
+ const nodeType = getStoreNodeType(node, isInstalled);
220
+ if (isNodeTypeHiddenFromPalette(nodeType, options.durableFunctionsEnabled)) return {
221
+ nodeType,
222
+ isPrebuilt: false,
223
+ isDraggable: false,
224
+ isHidden: true
225
+ };
226
+ const isInternalBuiltin = !!nodeType && nodeType.startsWith("internal:");
227
+ const isRegistryBacked = !!nodeType && AVAILABLE_NODES.some((availableNode) => availableNode.type === nodeType && (availableNode.publishType === "BUILTIN" || availableNode.publishType === void 0));
228
+ const isPrebuilt = !isInstalled && (isInternalBuiltin || isRegistryBacked);
229
+ return {
230
+ nodeType,
231
+ isPrebuilt,
232
+ isDraggable: !!nodeType && (isInstalled || isPrebuilt) && userSelectableOperationCount(nodeType) <= 1,
233
+ isHidden: false
234
+ };
235
+ }
236
+ /**
237
+ * The icon a store listing shows.
238
+ *
239
+ * Listings for the built-in nodes are seeded without an icon, so every one of
240
+ * them rendered as a letter tile in the Node Store while the canvas showed the
241
+ * node's real icon (BOFF-7651). The registry already carries that icon from
242
+ * the node's manifest, so a listing without its own falls back to it. A
243
+ * listing's own icon always wins; `null` means "no icon anywhere", and the
244
+ * caller draws its letter tile.
245
+ */
246
+ function getStoreNodeIcon(listingIcon, nodeType) {
247
+ if (listingIcon) return listingIcon;
248
+ if (!nodeType) return null;
249
+ return AVAILABLE_NODES.find((availableNode) => availableNode.type === nodeType)?.icon || null;
250
+ }
251
+ /**
252
+ * The version to show for a store node: the node's own version from its
253
+ * manifest (`nodeVersion`, then `identity.version`), falling back to the
254
+ * product's `manifestVersion`. The product field is usually the manifest
255
+ * *format* version ("1.0"), so showing it read as "v1.0" for every node
256
+ * (BOFF-11456).
257
+ */
258
+ function storeNodeDisplayVersion(manifest, manifestVersion) {
259
+ const record = typeof manifest === "string" ? parseManifest(manifest) : manifest;
260
+ if (typeof record === "object" && record !== null) {
261
+ const { nodeVersion, identity } = record;
262
+ if (typeof nodeVersion === "string" && nodeVersion.trim()) return nodeVersion.trim();
263
+ const identityVersion = identity?.version;
264
+ if (typeof identityVersion === "string" && identityVersion.trim()) return identityVersion.trim();
265
+ }
266
+ return manifestVersion?.trim() || void 0;
267
+ }
268
+ const MAJOR_MINOR_ONLY = /^\d+\.\d+$/;
269
+ /**
270
+ * Node versions are full semver: node images are tagged `major.minor.patch`
271
+ * and the sandbox picks one by the exact version a workflow node pins. A bare
272
+ * `major.minor` (the manifest format version, "1.0") therefore means
273
+ * `major.minor.0` (BOFF-11496). Other values come back trimmed.
274
+ */
275
+ function canonicalNodeVersion(version) {
276
+ const trimmed = version.trim();
277
+ return MAJOR_MINOR_ONLY.test(trimmed) ? `${trimmed}.0` : trimmed;
278
+ }
279
+ /**
280
+ * The version a store node installs as: its own version from the manifest,
281
+ * canonical, `1.0.0` when the product names none. Installs used to send the
282
+ * product's `manifestVersion` ("1.0"), which no node image carries
283
+ * (BOFF-11496).
284
+ */
285
+ function storeNodeInstallVersion(manifest, manifestVersion) {
286
+ return canonicalNodeVersion(storeNodeDisplayVersion(manifest, manifestVersion) ?? "1.0.0");
287
+ }
288
+ /**
289
+ * Whether an installed store node has a newer version: the installation's
290
+ * recorded version against the node's own current version, both canonical, so
291
+ * an installation recorded as "1.0" is not "outdated" against 1.0.0
292
+ * (BOFF-11496).
293
+ */
294
+ function isStoreNodeUpdateAvailable(installedVersion, manifest, manifestVersion) {
295
+ if (!installedVersion?.trim()) return false;
296
+ const latest = storeNodeDisplayVersion(manifest, manifestVersion);
297
+ if (!latest) return false;
298
+ return canonicalNodeVersion(installedVersion) !== canonicalNodeVersion(latest);
299
+ }
300
+ function parseManifest(text) {
301
+ try {
302
+ return JSON.parse(text);
303
+ } catch {
304
+ return;
305
+ }
306
+ }
307
+ /**
308
+ * Whether a store product must be left out of the builder's store listings
309
+ * because it is a gated node (durable-function or run-control) whose flag is
310
+ * off.
311
+ * Fails closed when `durableFunctionsEnabled` is omitted.
312
+ */
313
+ function isStoreNodeHiddenFromPalette(node, durableFunctionsEnabled = false) {
314
+ return isNodeTypeHiddenFromPalette(getStoreNodeType(node, false), durableFunctionsEnabled);
315
+ }
316
+ /**
317
+ * Drops the store products that must stay hidden from the builder. Returns
318
+ * the input array itself when nothing is hidden.
319
+ */
320
+ function filterStoreNodesForPalette(nodes, durableFunctionsEnabled = false) {
321
+ const hidden = (node) => isStoreNodeHiddenFromPalette(node, durableFunctionsEnabled);
322
+ if (!nodes.some(hidden)) return nodes;
323
+ return nodes.filter((node) => !hidden(node));
324
+ }
325
+
326
+ // src/hooks/store/useWorkflowTemplates.ts
327
+ /**
328
+ * Hook to browse workflow templates from the Store
329
+ *
330
+ * Fetches WORKFLOW type products from the global store service
331
+ * with rich preview data (screenshots, videos, etc.)
332
+ *
333
+ * @param searchTerm - Optional search query
334
+ * @param limit - Number of templates per page (default: 20)
335
+ * @param enabled - Whether to enable the query (default: true). Set to false to skip the query.
336
+ * @param category - Optional store category to filter by (exact match)
337
+ */
338
+ function useWorkflowTemplates(searchTerm, limit = 20, enabled = true, category) {
339
+ const client = useGlobalStoreClient();
340
+ const context = useContext(FluidGridsContext);
341
+ const workspaceId = context?.workspaceId ?? getFluidGridsConfig().workspaceId;
342
+ const organizationId = context?.organizationId;
343
+ const contextHeaders = useMemo(() => {
344
+ const headers = {};
345
+ if (organizationId) headers["x-organization-id"] = organizationId;
346
+ if (workspaceId) headers["x-workspace-id"] = workspaceId;
347
+ return headers;
348
+ }, [organizationId, workspaceId]);
349
+ const buildVariables = (offset) => ({ input: {
350
+ filter: {
351
+ type: "WORKFLOW",
352
+ status: "PUBLISHED",
353
+ search: searchTerm,
354
+ ...category ? { category } : {}
355
+ },
356
+ pagination: {
357
+ limit,
358
+ offset
359
+ },
360
+ sortBy: searchTerm ? "RELEVANCE" : "NEWEST"
361
+ } });
362
+ const { data, loading, error, fetchMore, refetch } = useQuery(BrowseWorkflowTemplatesDocument, {
363
+ client,
364
+ variables: buildVariables(0),
365
+ fetchPolicy: "cache-and-network",
366
+ skip: !enabled,
367
+ context: Object.keys(contextHeaders).length > 0 ? { headers: contextHeaders } : void 0
368
+ });
369
+ const templates = data?.browseStore?.products || [];
370
+ const totalCount = data?.browseStore?.totalCount || 0;
371
+ const hasMore = data?.browseStore?.hasMore || false;
372
+ const loadMore = () => {
373
+ if (!hasMore || loading) return;
374
+ return fetchMore({
375
+ variables: buildVariables(templates.length),
376
+ context: Object.keys(contextHeaders).length > 0 ? { headers: contextHeaders } : void 0,
377
+ updateQuery: (prev, { fetchMoreResult }) => {
378
+ if (!fetchMoreResult) return prev;
379
+ return { browseStore: {
380
+ ...fetchMoreResult.browseStore,
381
+ products: [...prev.browseStore?.products || [], ...fetchMoreResult.browseStore?.products || []]
382
+ } };
383
+ }
384
+ });
385
+ };
386
+ return {
387
+ templates,
388
+ totalCount,
389
+ hasMore,
390
+ loading,
391
+ error,
392
+ loadMore,
393
+ refetch
394
+ };
395
+ }
396
+
397
+ // src/hooks/store/useTemplateDetail.ts
398
+ const GET_WORKFLOW_TEMPLATE_DETAIL = gql`
399
+ query GetWorkflowTemplateDetail($id: ID!) {
400
+ product(id: $id) {
401
+ id
402
+ name
403
+ slug
404
+ type
405
+ description
406
+ longDescription
407
+ icon
408
+ screenshots
409
+ videoUrls
410
+ rating
411
+ reviewCount
412
+ downloads
413
+ viewCount
414
+ price
415
+ pricingModel
416
+ currency
417
+ featured
418
+ content
419
+ manifest
420
+ manifestVersion
421
+ authorName
422
+ license
423
+ category
424
+ tags
425
+ compatibleProducts
426
+ publisher {
427
+ id
428
+ name
429
+ email
430
+ websiteUrl
431
+ logoUrl
432
+ isVerified
433
+ }
434
+ createdAt
435
+ updatedAt
436
+ publishedAt
437
+ }
438
+ }
439
+ `;
440
+ /**
441
+ * Hook to lazily fetch full details of a workflow template from the store.
442
+ *
443
+ * Unlike `useWorkflowTemplates` which fetches browse-level metadata,
444
+ * this hook fetches the full product detail including the `content` field
445
+ * which contains the workflow definitionJson.
446
+ *
447
+ * Uses a manual query (not useQuery) to only fetch when explicitly requested.
448
+ */
449
+ function useTemplateDetail() {
450
+ const client = useGlobalStoreClient();
451
+ const [loading, setLoading] = useState(false);
452
+ const [error, setError] = useState(null);
453
+ const context = useContext(FluidGridsContext);
454
+ const workspaceId = context?.workspaceId ?? getFluidGridsConfig().workspaceId;
455
+ const organizationId = context?.organizationId;
456
+ const contextHeaders = useMemo(() => {
457
+ const headers = {};
458
+ if (organizationId) headers["x-organization-id"] = organizationId;
459
+ if (workspaceId) headers["x-workspace-id"] = workspaceId;
460
+ return headers;
461
+ }, [organizationId, workspaceId]);
462
+ return {
463
+ fetchTemplateDetail: useCallback(async (templateId) => {
464
+ setLoading(true);
465
+ setError(null);
466
+ try {
467
+ const result = await client.query({
468
+ query: GET_WORKFLOW_TEMPLATE_DETAIL,
469
+ variables: { id: templateId },
470
+ fetchPolicy: "network-only",
471
+ context: Object.keys(contextHeaders).length > 0 ? { headers: contextHeaders } : void 0
472
+ });
473
+ if (result.error) throw result.error;
474
+ return result.data?.product ?? null;
475
+ } catch (err) {
476
+ const fetchError = err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to fetch template detail");
477
+ setError(fetchError);
478
+ console.error("[useTemplateDetail] Error:", err);
479
+ return null;
480
+ } finally {
481
+ setLoading(false);
482
+ }
483
+ }, [client, contextHeaders]),
484
+ loading,
485
+ error
486
+ };
487
+ }
488
+
489
+ // src/utils/template-input-applicator.ts
490
+ /**
491
+ * Template Input Applicator
492
+ *
493
+ * Walks a workflow definitionJson and replaces ${variableName} placeholders
494
+ * with user-provided values. Used during template import to apply
495
+ * user-configurable inputs before creating the workflow.
496
+ */
497
+ /**
498
+ * Deep-clone an object using structured clone.
499
+ * Falls back to JSON parse/stringify for environments without structuredClone.
500
+ */
501
+ function deepClone(obj) {
502
+ if (typeof structuredClone === "function") return structuredClone(obj);
503
+ return JSON.parse(JSON.stringify(obj));
504
+ }
505
+ /**
506
+ * Apply template input values to a definitionJson by replacing
507
+ * ${variableName} placeholders in string values throughout the object tree.
508
+ *
509
+ * @param definitionJson - The workflow definition JSON to process
510
+ * @param inputs - Map of variable names to user-provided values
511
+ * @returns A new definitionJson with placeholders replaced
512
+ *
513
+ * @example
514
+ * ```ts
515
+ * const definition = {
516
+ * nodes: [{
517
+ * data: { url: "${slack_webhook_url}", message: "Hello ${name}" }
518
+ * }]
519
+ * };
520
+ * const result = applyTemplateInputs(definition, {
521
+ * slack_webhook_url: "https://hooks.slack.com/...",
522
+ * name: "World"
523
+ * });
524
+ * // result.nodes[0].data.url === "https://hooks.slack.com/..."
525
+ * // result.nodes[0].data.message === "Hello World"
526
+ * ```
527
+ */
528
+ function applyTemplateInputs(definitionJson, inputs) {
529
+ if (!inputs || Object.keys(inputs).length === 0) return definitionJson;
530
+ return walkAndReplace(deepClone(definitionJson), inputs);
531
+ }
532
+ /**
533
+ * Recursively walk an object/array and replace ${variableName} placeholders
534
+ * in string values.
535
+ */
536
+ function walkAndReplace(obj, inputs) {
537
+ if (typeof obj === "string") return replaceVariables(obj, inputs);
538
+ if (Array.isArray(obj)) return obj.map((item) => walkAndReplace(item, inputs));
539
+ if (obj !== null && typeof obj === "object") {
540
+ const result = {};
541
+ for (const [key, value] of Object.entries(obj)) result[key] = walkAndReplace(value, inputs);
542
+ return result;
543
+ }
544
+ return obj;
545
+ }
546
+ /**
547
+ * Replace ${variableName} patterns in a string with provided values.
548
+ *
549
+ * Supports:
550
+ * - ${variableName} — standard replacement
551
+ * - ${variableName || 'default'} — fallback to default if variable not provided
552
+ */
553
+ function replaceVariables(str, inputs) {
554
+ return str.replace(/\$\{([^}]+)\}/g, (_match, expression) => {
555
+ const trimmed = expression.trim();
556
+ const fallbackMatch = trimmed.match(/^(\w+)\s*\|\|\s*['"]?(.+?)['"]?$/);
557
+ if (fallbackMatch) {
558
+ const varName = fallbackMatch[1];
559
+ const fallback = fallbackMatch[2];
560
+ if (varName in inputs) return String(inputs[varName]);
561
+ return fallback;
562
+ }
563
+ if (trimmed in inputs) return String(inputs[trimmed]);
564
+ return _match;
565
+ });
566
+ }
567
+
568
+ // src/utils/template-import-flow.ts
569
+ function isRecord(value) {
570
+ return typeof value === "object" && value !== null && !Array.isArray(value);
571
+ }
572
+ function graphOf(value) {
573
+ if (!Array.isArray(value.nodes)) return null;
574
+ return {
575
+ nodes: value.nodes,
576
+ edges: Array.isArray(value.edges) ? value.edges : [],
577
+ name: typeof value.name === "string" ? value.name : void 0
578
+ };
579
+ }
580
+ /**
581
+ * The canvas of a WORKFLOW product that keeps it in its manifest instead of
582
+ * `content.definitionJson`: either a flat `{ nodes, edges }` manifest, or the
583
+ * Store Listing JSON editor's `manifest.jsonContent` string. Null when the
584
+ * manifest holds neither.
585
+ */
586
+ function manifestTemplateGraph(manifest) {
587
+ if (!isRecord(manifest)) return null;
588
+ const direct = graphOf(manifest);
589
+ if (direct) return direct;
590
+ if (typeof manifest.jsonContent !== "string") return null;
591
+ try {
592
+ const parsed = JSON.parse(manifest.jsonContent);
593
+ return isRecord(parsed) ? graphOf(parsed) : null;
594
+ } catch {
595
+ return null;
596
+ }
597
+ }
598
+ /**
599
+ * The template content a product carries: `content` when it holds a
600
+ * `definitionJson`, otherwise a content built from the manifest's canvas,
601
+ * with its required nodes worked out by `requiredNodeTypesOf` (a failed
602
+ * lookup leaves them empty, which skips the node check). Null when the
603
+ * product has no workflow at all.
604
+ */
605
+ async function resolveTemplateContent(product, requiredNodeTypesOf) {
606
+ const content = product.content;
607
+ if (content?.definitionJson) return content;
608
+ const graph = manifestTemplateGraph(product.manifest);
609
+ if (!graph) return null;
610
+ let requiredNodeTypes = [];
611
+ try {
612
+ requiredNodeTypes = await requiredNodeTypesOf(graph.nodes);
613
+ } catch {
614
+ requiredNodeTypes = [];
615
+ }
616
+ return {
617
+ definitionJson: {
618
+ nodes: graph.nodes,
619
+ edges: graph.edges
620
+ },
621
+ requiredNodeTypes,
622
+ defaultWorkflowName: graph.name ?? product.name
623
+ };
624
+ }
625
+ /**
626
+ * The canvas to create a copy from. Some templates store a whole API
627
+ * response as their definition (`{ data: { workflowDefinition: {
628
+ * definitionJson } } }`); the canvas is the inner `definitionJson`.
629
+ */
630
+ function unwrapTemplateDefinition(definitionJson) {
631
+ const data = definitionJson.data;
632
+ if (!isRecord(data)) return definitionJson;
633
+ const nested = isRecord(data.workflowDefinition) ? data.workflowDefinition.definitionJson : void 0;
634
+ return isRecord(nested) ? nested : definitionJson;
635
+ }
636
+ /**
637
+ * Whether "Use this template" must stop at the configuration dialog before
638
+ * creating the copy: the template asks for inputs or connections, or one of
639
+ * its nodes is not installed (free to install, paid, or not offered by the
640
+ * store at all).
641
+ */
642
+ function needsTemplateConfiguration(content, nodeCheck) {
643
+ return (content.requiredInputs?.length ?? 0) > 0 || (content.requiredCredentialTypes?.length ?? 0) > 0 || nodeCheck.missing.length > 0 || nodeCheck.missingPaid.length > 0 || (nodeCheck.unavailable?.length ?? 0) > 0;
644
+ }
645
+ /** The free nodes to install before the copy is created. */
646
+ function nodesToInstall(nodeCheck) {
647
+ return (nodeCheck?.missing ?? []).filter((node) => !!node.storeProductId);
648
+ }
649
+ /**
650
+ * Install nodes one at a time, in order, reporting progress before each.
651
+ * One at a time on purpose: the dialog shows "installing i of n", and every
652
+ * install writes the same workspace's installation records in the store.
653
+ * A failed install is reported to `onFailure` and the rest still run, so the
654
+ * copy can be created; the user installs the node later from the Node Store.
655
+ * Resolves to the node types that installed.
656
+ */
657
+ async function installNodesInOrder(nodes, install, onProgress, onFailure) {
658
+ const installed = [];
659
+ await nodes.reduce((previous, node, index) => previous.then(async () => {
660
+ onProgress({
661
+ current: index + 1,
662
+ total: nodes.length,
663
+ nodeName: node.displayName || node.nodeType
664
+ });
665
+ try {
666
+ await install(node);
667
+ installed.push(node.nodeType);
668
+ } catch (error) {
669
+ onFailure(node, error);
670
+ }
671
+ }), Promise.resolve());
672
+ return installed;
673
+ }
674
+
675
+ // src/hooks/store/useImportWorkflowTemplate.ts
676
+ const IMPORT_WORKFLOW_TEMPLATE = gql`
677
+ mutation ImportWorkflowTemplate($input: ImportWorkflowTemplateInput!) {
678
+ importWorkflowTemplate(input: $input) {
679
+ success
680
+ message
681
+ workflow {
682
+ id
683
+ workflowKey
684
+ version
685
+ name
686
+ description
687
+ status
688
+ definitionJson
689
+ tags
690
+ category
691
+ icon
692
+ createdAt
693
+ }
694
+ }
695
+ }
696
+ `;
697
+ const RECORD_DOWNLOAD = gql`
698
+ mutation RecordDownload($productId: String!) {
699
+ recordDownload(productId: $productId) {
700
+ id
701
+ downloadedAt
702
+ }
703
+ }
704
+ `;
705
+ const WORKFLOW_DEFINITIONS = gql`
706
+ query WorkflowDefinitions($input: ListWorkflowDefinitionsInput!) {
707
+ workflowDefinitions(input: $input) {
708
+ items {
709
+ id
710
+ workflowKey
711
+ name
712
+ version
713
+ status
714
+ }
715
+ total
716
+ hasMore
717
+ }
718
+ }
719
+ `;
720
+ /**
721
+ * Hook to orchestrate importing a workflow template from the store.
722
+ *
723
+ * It fetches template detail, parses content and creates the copy:
724
+ * 1. Fetch full template detail from global store (lazy load content)
725
+ * 2. Apply user-provided template inputs to definitionJson
726
+ * 3. Create workflow via importWorkflowTemplate mutation (workspace gateway)
727
+ * 4. Record the download and the installation in the store
728
+ *
729
+ * The node check, the configuration dialog and installing missing nodes are
730
+ * done by `useTemplateImportFlow`, which every "Use this template" button
731
+ * goes through (BOFF-11153).
732
+ */
733
+ function useImportWorkflowTemplate() {
734
+ const [importing, setImporting] = useState(false);
735
+ const [error, setError] = useState(null);
736
+ const { fetchTemplateDetail, loading: fetchingDetail } = useTemplateDetail();
737
+ const globalClient = useGlobalStoreClient();
738
+ const context = useContext(FluidGridsContext);
739
+ const workspaceId = context?.workspaceId ?? getFluidGridsConfig().workspaceId;
740
+ const bus = useEventBus();
741
+ const getTemplateDetail = useCallback(async (templateId) => {
742
+ return fetchTemplateDetail(templateId);
743
+ }, [fetchTemplateDetail]);
744
+ /**
745
+ * Parse the template content from a StoreProduct.
746
+ * Returns null if the product doesn't have valid workflow template content.
747
+ */
748
+ const parseTemplateContent = useCallback((template) => {
749
+ try {
750
+ const content = template.content;
751
+ if (!content?.definitionJson) {
752
+ console.warn("[useImportWorkflowTemplate] Template has no definitionJson in content:", template.id);
753
+ return null;
754
+ }
755
+ return content;
756
+ } catch {
757
+ console.error("[useImportWorkflowTemplate] Failed to parse template content:", template.id);
758
+ return null;
759
+ }
760
+ }, []);
761
+ return {
762
+ /** Fetch full template content from store (lazy load) */
763
+ getTemplateDetail,
764
+ /** Parse WorkflowTemplateContent from a StoreProduct */
765
+ parseTemplateContent,
766
+ /** Full import: apply inputs -> create workflow -> record analytics */
767
+ importTemplate: useCallback(async (template, options) => {
768
+ setImporting(true);
769
+ setError(null);
770
+ try {
771
+ const content = options?.content ?? parseTemplateContent(template);
772
+ if (!content) throw new Error("Template does not contain valid workflow definition data");
773
+ let definitionJson = unwrapTemplateDefinition(deepClone(content.definitionJson));
774
+ if (options?.templateInputs) definitionJson = applyTemplateInputs(definitionJson, options.templateInputs);
775
+ const config = getFluidGridsConfig();
776
+ const importContext = {
777
+ ...options?.context || {},
778
+ ...config.productId ? { productId: config.productId } : {},
779
+ ...config.productName ? { productName: config.productName } : {}
780
+ };
781
+ const client = getFluidGridsClient();
782
+ const importResult = throwIfResultError(await client.mutate({
783
+ mutation: IMPORT_WORKFLOW_TEMPLATE,
784
+ variables: { input: {
785
+ definitionJson,
786
+ name: options?.name || content.defaultWorkflowName || template.name,
787
+ description: options?.description || template.description,
788
+ sourceTemplateId: template.id,
789
+ sourceTemplateName: template.name,
790
+ tags: content.defaultTags || template.tags || [],
791
+ category: content.defaultCategory || template.category || void 0,
792
+ icon: template.icon || void 0,
793
+ projectId: options?.projectId,
794
+ context: importContext
795
+ } },
796
+ refetchQueries: [WORKFLOW_DEFINITIONS]
797
+ })).data?.importWorkflowTemplate;
798
+ if (!importResult?.success || !importResult.workflow) throw new Error(importResult?.message || "Failed to create workflow from template");
799
+ try {
800
+ throwIfResultError(await globalClient.mutate({
801
+ mutation: RECORD_DOWNLOAD,
802
+ variables: { productId: template.id }
803
+ }));
804
+ } catch {
805
+ console.warn("[useImportWorkflowTemplate] Failed to record store download");
806
+ }
807
+ try {
808
+ throwIfResultError(await globalClient.mutate({
809
+ mutation: InstallWorkflowNodeDocument,
810
+ variables: { input: {
811
+ productId: template.id,
812
+ workspaceId,
813
+ organizationId: context?.tenantId ?? getFluidGridsConfig().tenantId,
814
+ version: template.manifestVersion || "1.0.0",
815
+ manifest: template.manifest,
816
+ metadata: {
817
+ type: "WORKFLOW",
818
+ sourceTemplateId: template.id,
819
+ sourceTemplateName: template.name,
820
+ importedWorkflowKey: importResult.workflow.workflowKey,
821
+ importedWorkflowVersion: importResult.workflow.version
822
+ }
823
+ } }
824
+ }));
825
+ } catch {
826
+ console.warn("[useImportWorkflowTemplate] Failed to record app installation");
827
+ }
828
+ emitWorkflowCreated(bus, {
829
+ workspaceId,
830
+ workflowId: importResult.workflow.id,
831
+ source: "template",
832
+ definition: definitionJson
833
+ });
834
+ emitTemplateUsed(bus, {
835
+ workspaceId,
836
+ templateId: template.id,
837
+ category: content.defaultCategory || template.category
838
+ });
839
+ return {
840
+ success: true,
841
+ workflow: importResult.workflow,
842
+ message: importResult.message || "Template imported successfully",
843
+ installedNodes: [],
844
+ requiredCredentials: content.requiredCredentialTypes || []
845
+ };
846
+ } catch (err) {
847
+ const importError = err instanceof Error ? err : /* @__PURE__ */ new Error("Template import failed");
848
+ setError(importError);
849
+ console.error("[useImportWorkflowTemplate] Import failed:", err);
850
+ return {
851
+ success: false,
852
+ workflow: null,
853
+ message: importError.message,
854
+ installedNodes: [],
855
+ requiredCredentials: [],
856
+ error: importError
857
+ };
858
+ } finally {
859
+ setImporting(false);
860
+ }
861
+ }, [
862
+ parseTemplateContent,
863
+ globalClient,
864
+ workspaceId,
865
+ context?.tenantId,
866
+ bus
867
+ ]),
868
+ /** Whether we're fetching template detail */
869
+ fetchingDetail,
870
+ /** Whether we're importing */
871
+ importing,
872
+ /** Last error */
873
+ error
874
+ };
875
+ }
876
+
877
+ // src/utils/workflow-node-check.ts
878
+ /**
879
+ * Workflow node dependency checking.
880
+ *
881
+ * Shared logic for detecting node types used by a workflow that are not
882
+ * installed in the current workspace, resolving them to store products, and
883
+ * classifying them as installed / missing (free installable) / missingPaid /
884
+ * unavailable (not offered by the store, so nothing can install them).
885
+ *
886
+ * Used by:
887
+ * - Template import flow (desktop-sidebar-fragment)
888
+ * - Shared-workflow copy flow (WorkflowItem)
889
+ * - AI-assistant-created workflow open flow (WorkflowItem)
890
+ */
891
+ /**
892
+ * Canvas-only node types: they live on the React Flow canvas but are never
893
+ * executed, never registered in NODE_CONFIGS and never published to the store.
894
+ *
895
+ * They must be excluded from the dependency check. A sticky note reaching it
896
+ * was reported as `sticky-note v1.0 — Not installed` in the Missing Nodes
897
+ * dialog on every open of an affected workflow, and because no store product
898
+ * backs it the Install action could only ever be a no-op (BOFF-7267).
899
+ *
900
+ * Kept as a literal set rather than importing STICKY_NOTE_TYPE from the
901
+ * builder so this module stays free of builder-component imports.
902
+ */
903
+ const CANVAS_ONLY_NODE_TYPES = /* @__PURE__ */ new Set(["sticky-note"]);
904
+ /**
905
+ * Resolve the set of unique executable node types used by a workflow.
906
+ *
907
+ * Canvas-only annotations (see CANVAS_ONLY_NODE_TYPES) are skipped: they carry
908
+ * no dependency, so including them produces a permanently unsatisfiable
909
+ * "missing node".
910
+ */
911
+ function getWorkflowNodeTypes(nodes) {
912
+ return [...new Set(nodes.map((n) => n.type).filter((t) => Boolean(t)).filter((t) => !CANVAS_ONLY_NODE_TYPES.has(t)))];
913
+ }
914
+ /**
915
+ * Node types per store lookup: global-store-svc answers for at most 100 at a
916
+ * time. A workflow misses far fewer; more are asked in parallel requests.
917
+ */
918
+ const STORE_NODE_LOOKUP_BATCH_SIZE = 100;
919
+ /** The store context of FluidGrids node products, as the Node Store browses them. */
920
+ const STORE_NODE_PRODUCT_CONTEXT = "fluidgrids";
921
+ /**
922
+ * The store products that offer any of `nodeTypes`, at most
923
+ * `STORE_NODE_LOOKUP_BATCH_SIZE` types per request, the requests in parallel.
924
+ * Blank and repeated types are asked once.
925
+ */
926
+ async function fetchStoreNodeProducts(nodeTypes, fetchProducts) {
927
+ const wanted = [...new Set(nodeTypes.filter(Boolean))];
928
+ const batches = [];
929
+ for (let i = 0; i < wanted.length; i += 100) batches.push(wanted.slice(i, i + 100));
930
+ return (await Promise.all(batches.map(fetchProducts))).flatMap((products) => [...products ?? []]);
931
+ }
932
+ function publishedAtMs(product) {
933
+ const ms = product.publishedAt ? Date.parse(product.publishedAt) : NaN;
934
+ return Number.isNaN(ms) ? Number.NEGATIVE_INFINITY : ms;
935
+ }
936
+ /**
937
+ * Order two store products that offer the same nodeType; a negative result
938
+ * means `a` is the better match.
939
+ *
940
+ * 1. A product linked to a devportal application (`productId` set) wins. Only
941
+ * those have an installable published version; the unlinked rows are
942
+ * orphaned seed rows such as `app-gmail-reader-001` that cannot install.
943
+ * 2. Then more downloads, 3. then the most recently published, 4. then the
944
+ * product id, so the choice never depends on the order the store returned.
945
+ */
946
+ function compareStoreNodeCandidates(a, b) {
947
+ const aLinked = Boolean(a.productId);
948
+ if (aLinked !== Boolean(b.productId)) return aLinked ? -1 : 1;
949
+ const downloads = (b.downloads ?? 0) - (a.downloads ?? 0);
950
+ if (downloads !== 0) return downloads;
951
+ const aPublished = publishedAtMs(a);
952
+ const bPublished = publishedAtMs(b);
953
+ if (aPublished !== bPublished) return aPublished > bPublished ? -1 : 1;
954
+ if (a.id === b.id) return 0;
955
+ return a.id < b.id ? -1 : 1;
956
+ }
957
+ /**
958
+ * Map nodeType -> best store product (see compareStoreNodeCandidates).
959
+ * Pure function, independent of the order of `products`.
960
+ */
961
+ function buildStoreNodeTypeMap(products) {
962
+ const nodeTypeToProduct = /* @__PURE__ */ new Map();
963
+ for (const product of products) {
964
+ const nodeType = product.manifest?.nodeType;
965
+ if (typeof nodeType !== "string" || !nodeType) continue;
966
+ const current = nodeTypeToProduct.get(nodeType);
967
+ if (!current || compareStoreNodeCandidates(product, current) < 0) nodeTypeToProduct.set(nodeType, product);
968
+ }
969
+ return nodeTypeToProduct;
970
+ }
971
+ /**
972
+ * Ask the global store which products offer `nodeTypes` and map each type to the
973
+ * best one (see compareStoreNodeCandidates). Only the asked types travel: the
974
+ * check used to page through the whole node catalog for them (BOFF-11754).
975
+ */
976
+ async function fetchStoreNodeTypeMap(nodeTypes) {
977
+ const storeClient = getGlobalStoreClient();
978
+ if (!storeClient || nodeTypes.length === 0) return /* @__PURE__ */ new Map();
979
+ return buildStoreNodeTypeMap(await fetchStoreNodeProducts(nodeTypes, async (batch) => {
980
+ return (await storeClient.query({
981
+ query: StoreNodeProductsByTypeDocument,
982
+ variables: {
983
+ nodeTypes: batch,
984
+ compatibleWith: STORE_NODE_PRODUCT_CONTEXT
985
+ },
986
+ fetchPolicy: "cache-first"
987
+ })).data?.storeNodeProductsByType;
988
+ }));
989
+ }
990
+ /**
991
+ * One required-node entry of a template (BOFF-11496): a type the registry
992
+ * already has is BUILTIN at its registry version; any other type is a STORE
993
+ * node at the node's own version from its store product. Versions are
994
+ * canonical, never the product's manifest format version ("1.0").
995
+ */
996
+ function requiredNodeEntry(type, registered, product) {
997
+ if (registered) return {
998
+ nodeType: type,
999
+ nodeVersion: canonicalNodeVersion(registered.version || "1.0.0"),
1000
+ publishType: "BUILTIN",
1001
+ displayName: type
1002
+ };
1003
+ return {
1004
+ nodeType: type,
1005
+ nodeVersion: storeNodeInstallVersion(product?.manifest, product?.manifestVersion),
1006
+ publishType: "STORE",
1007
+ storeProductId: product?.id,
1008
+ displayName: product?.name ?? type,
1009
+ requiresPurchase: product !== void 0 && product.pricingModel !== "FREE"
1010
+ };
1011
+ }
1012
+ /**
1013
+ * Build the list of required node types for a workflow.
1014
+ *
1015
+ * - BUILTIN / already-installed types are marked BUILTIN.
1016
+ * - Missing types are looked up in the store; if found, they are marked STORE
1017
+ * with storeProductId and requiresPurchase.
1018
+ * - Missing types not found in the store are still reported as STORE without
1019
+ * a storeProductId so the UI can show "not installed".
1020
+ */
1021
+ async function buildRequiredNodeTypes(nodes) {
1022
+ const workflowNodeTypes = getWorkflowNodeTypes(nodes);
1023
+ if (workflowNodeTypes.length === 0) return [];
1024
+ const isRegistered = (type) => NODE_CONFIGS.some((c) => c.type === type);
1025
+ await ensureRegistryNodeTypesLoaded(workflowNodeTypes);
1026
+ const missingTypes = workflowNodeTypes.filter((type) => !isRegistered(type));
1027
+ let nodeTypeToProduct = /* @__PURE__ */ new Map();
1028
+ if (missingTypes.length > 0) [, nodeTypeToProduct] = await Promise.all([ensureInstalledNodeTypesLoaded(missingTypes), fetchStoreNodeTypeMap(missingTypes)]);
1029
+ return workflowNodeTypes.map((type) => {
1030
+ const registered = NODE_CONFIGS.find((c) => c.type === type);
1031
+ return requiredNodeEntry(type, registered ? { version: registered.defaultData.version } : void 0, nodeTypeToProduct.get(type));
1032
+ });
1033
+ }
1034
+ /**
1035
+ * Classify required nodes against the set of installed store app IDs.
1036
+ *
1037
+ * Pure function — easy to unit test.
1038
+ */
1039
+ function buildNodeCheck(requiredNodeTypes, installedAppIds) {
1040
+ const installed = requiredNodeTypes.filter((n) => n.publishType === "BUILTIN" || n.storeProductId && installedAppIds.has(n.storeProductId));
1041
+ const notInstalled = requiredNodeTypes.filter((n) => n.publishType !== "BUILTIN" && (!n.storeProductId || !installedAppIds.has(n.storeProductId)));
1042
+ const unavailable = notInstalled.filter((n) => !n.storeProductId);
1043
+ const installable = notInstalled.filter((n) => !!n.storeProductId);
1044
+ const missing = installable.filter((n) => !n.requiresPurchase);
1045
+ const missingPaid = installable.filter((n) => !!n.requiresPurchase);
1046
+ return {
1047
+ installed,
1048
+ missing,
1049
+ missingPaid,
1050
+ unavailable,
1051
+ canImport: missingPaid.length === 0
1052
+ };
1053
+ }
1054
+
1055
+ // src/hooks/store/useTemplateImportFlow.tsx
1056
+ const LazyTemplateConfigurationModal = lazy(() => import("./TemplateConfigurationModal-BhGWIzpm.js").then((m) => ({ default: m.TemplateConfigurationModal })));
1057
+ /**
1058
+ * "Use this template", the same everywhere it is offered: the builder's
1059
+ * template panel, the Templates page and the template preview page
1060
+ * (BOFF-11153). Before this, only the builder panel checked the template's
1061
+ * nodes and asked for its inputs; the two pages created the copy directly.
1062
+ *
1063
+ * 1. Load the template's full detail and resolve its content.
1064
+ * 2. Check its required nodes against the workspace's installed nodes.
1065
+ * 3. When it asks for inputs or connections, or a node is not installed,
1066
+ * show the configuration dialog; otherwise go straight on.
1067
+ * 4. Install the missing free nodes, create the copy with the user's inputs,
1068
+ * and open it (`onImported`).
1069
+ */
1070
+ function useTemplateImportFlow({ onImported, onQuotaExhausted }) {
1071
+ const tr = useTr();
1072
+ const { bus, workspaceId } = useFluidGridsAnalytics();
1073
+ const { installedAppIds } = useInstalledNodes();
1074
+ const { getTemplateDetail, importTemplate } = useImportWorkflowTemplate();
1075
+ const [pending, setPending] = useState(null);
1076
+ const [busy, setBusy] = useState(false);
1077
+ const [installProgress, setInstallProgress] = useState(null);
1078
+ const installMissingNodes = useCallback(async (nodeCheck) => {
1079
+ const nodes = nodesToInstall(nodeCheck);
1080
+ const storeClient = getGlobalStoreClient();
1081
+ if (nodes.length === 0 || !storeClient) return;
1082
+ const config = getFluidGridsConfig();
1083
+ await installNodesInOrder(nodes, async (node) => {
1084
+ throwIfResultError(await storeClient.mutate({
1085
+ mutation: InstallWorkflowNodeDocument,
1086
+ variables: { input: {
1087
+ productId: node.storeProductId ?? "",
1088
+ workspaceId: config.workspaceId,
1089
+ organizationId: config.tenantId,
1090
+ version: node.nodeVersion,
1091
+ manifest: null
1092
+ } }
1093
+ }));
1094
+ emitNodeInstalled(bus, {
1095
+ workspaceId,
1096
+ nodeType: node.nodeType,
1097
+ source: "store"
1098
+ });
1099
+ }, setInstallProgress, (node, err) => console.warn(`[useTemplateImportFlow] Failed to install node ${node.nodeType}:`, err));
1100
+ setInstallProgress(null);
1101
+ }, [bus, workspaceId]);
1102
+ const createCopy = useCallback(async (template, content, nodeCheck, overrides) => {
1103
+ setBusy(true);
1104
+ try {
1105
+ await installMissingNodes(nodeCheck);
1106
+ toast.loading(tr("fluidgrids.builder.desktopSidebar.creatingWorkflowToast", "Creating workflow..."), { id: "template-import" });
1107
+ const result = await importTemplate(template, {
1108
+ content,
1109
+ name: overrides?.name,
1110
+ templateInputs: overrides?.templateInputs
1111
+ });
1112
+ toast.dismiss("template-import");
1113
+ if (result.success && result.workflow) {
1114
+ toast.success(tr("fluidgrids.builder.desktopSidebar.workflowCreatedToast", "Workflow created from template!"), { description: `"${result.workflow.name}" is ready to edit` });
1115
+ if ((content.requiredCredentialTypes?.length ?? 0) > 0) toast.info(tr("fluidgrids.builder.desktopSidebar.credentialsNeededToast", "Credentials needed"), {
1116
+ description: `Configure ${content.requiredCredentialTypes?.join(", ")} in the Credentials page`,
1117
+ duration: 6e3
1118
+ });
1119
+ onImported(result.workflow);
1120
+ return true;
1121
+ }
1122
+ if (onQuotaExhausted && isQuotaExhaustedFailure(result.error?.cause ?? result.error)) {
1123
+ onQuotaExhausted();
1124
+ return false;
1125
+ }
1126
+ toast.error(tr("fluidgrids.builder.desktopSidebar.importTemplateErrorToast", "Failed to import template"), { description: result.message || tr("fluidgrids.builder.desktopSidebar.importTemplateErrorFallback", "Unknown error during import") });
1127
+ return false;
1128
+ } catch (err) {
1129
+ toast.dismiss("template-import");
1130
+ toast.error(tr("fluidgrids.builder.desktopSidebar.importFailedToast", "Import failed"), { description: err instanceof Error ? err.message : tr("fluidgrids.builder.desktopSidebar.importFailedFallback", "Unknown error") });
1131
+ return false;
1132
+ } finally {
1133
+ setInstallProgress(null);
1134
+ setBusy(false);
1135
+ }
1136
+ }, [
1137
+ importTemplate,
1138
+ installMissingNodes,
1139
+ onImported,
1140
+ onQuotaExhausted,
1141
+ tr
1142
+ ]);
1143
+ const start = useCallback(async (template) => {
1144
+ setBusy(true);
1145
+ toast.loading(tr("fluidgrids.builder.desktopSidebar.loadingTemplateToast", "Loading template..."), { id: "template-load" });
1146
+ try {
1147
+ const full = await getTemplateDetail(template.id);
1148
+ toast.dismiss("template-load");
1149
+ if (!full) {
1150
+ toast.error(tr("fluidgrids.builder.desktopSidebar.templateDetailsErrorToast", "Failed to load template details"));
1151
+ return;
1152
+ }
1153
+ const content = await resolveTemplateContent(full, buildRequiredNodeTypes);
1154
+ if (!content) {
1155
+ toast.error(tr("fluidgrids.builder.desktopSidebar.invalidTemplateDataToast", "Template does not contain valid workflow data"));
1156
+ return;
1157
+ }
1158
+ const nodeCheck = buildNodeCheck(content.requiredNodeTypes ?? [], installedAppIds);
1159
+ if (needsTemplateConfiguration(content, nodeCheck)) {
1160
+ setPending({
1161
+ template: full,
1162
+ content,
1163
+ nodeCheck
1164
+ });
1165
+ return;
1166
+ }
1167
+ await createCopy(full, content, nodeCheck);
1168
+ } catch (err) {
1169
+ toast.dismiss("template-load");
1170
+ toast.error(tr("fluidgrids.builder.desktopSidebar.genericErrorToast", "Something went wrong"), { description: err instanceof Error ? err.message : tr("fluidgrids.builder.desktopSidebar.genericErrorFallback", "Failed to process template") });
1171
+ } finally {
1172
+ setBusy(false);
1173
+ }
1174
+ }, [
1175
+ createCopy,
1176
+ getTemplateDetail,
1177
+ installedAppIds,
1178
+ tr
1179
+ ]);
1180
+ const confirm = useCallback(async (config) => {
1181
+ if (!pending?.nodeCheck.canImport) return;
1182
+ if (await createCopy(pending.template, pending.content, pending.nodeCheck, config)) setPending(null);
1183
+ }, [createCopy, pending]);
1184
+ return {
1185
+ start,
1186
+ dialog: pending ? /* @__PURE__ */ jsx(Suspense, {
1187
+ fallback: null,
1188
+ children: /* @__PURE__ */ jsx(LazyTemplateConfigurationModal, {
1189
+ open: true,
1190
+ onOpenChange: (open) => {
1191
+ if (!open && !busy) setPending(null);
1192
+ },
1193
+ template: pending.template,
1194
+ content: pending.content,
1195
+ nodeCheckResult: pending.nodeCheck,
1196
+ onConfirm: confirm,
1197
+ importing: busy,
1198
+ installProgress
1199
+ })
1200
+ }) : null,
1201
+ busy
1202
+ };
1203
+ }
1204
+
1205
+ // src/ui/badge.tsx
1206
+ const badgeVariants = cva("inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", {
1207
+ variants: { variant: {
1208
+ default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
1209
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
1210
+ destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
1211
+ outline: "text-foreground",
1212
+ success: "border-transparent bg-status-success-bg-subtle text-status-success-text",
1213
+ warning: "border-transparent bg-status-warning-bg-subtle text-status-warning-text",
1214
+ info: "border-transparent bg-status-info-bg-subtle text-status-info-text"
1215
+ } },
1216
+ defaultVariants: { variant: "default" }
1217
+ });
1218
+ function Badge({ className, variant, ...props }) {
1219
+ return /* @__PURE__ */ jsx("div", {
1220
+ className: cn(badgeVariants({ variant }), className),
1221
+ ...props
1222
+ });
1223
+ }
1224
+
1225
+ // src/utils/template-preview.ts
1226
+ const ICONIFY_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:[-_][a-z0-9]+)*$/i;
1227
+ /** True when the value is an Iconify `prefix:name` icon, not a URL. */
1228
+ function isIconifyName(value) {
1229
+ return ICONIFY_NAME.test(value) && !/^(https?|data|blob):/i.test(value);
1230
+ }
1231
+ /**
1232
+ * The preview for a template: its first screenshot, else its icon, which is
1233
+ * shown as an image only when it is not an Iconify name.
1234
+ */
1235
+ function templatePreview(template) {
1236
+ const screenshot = template.screenshots?.find((shot) => typeof shot === "string" && shot.trim() !== "");
1237
+ if (screenshot) return {
1238
+ kind: "image",
1239
+ src: screenshot
1240
+ };
1241
+ const icon = template.icon?.trim();
1242
+ if (!icon) return { kind: "none" };
1243
+ return isIconifyName(icon) ? {
1244
+ kind: "icon",
1245
+ name: icon
1246
+ } : {
1247
+ kind: "image",
1248
+ src: icon
1249
+ };
1250
+ }
1251
+
1252
+ // src/builder/components/flow-builder/components/templates/TemplateCard.tsx
1253
+ function formatNumber(count) {
1254
+ if (count >= 1e6) return `${(count / 1e6).toFixed(1)}M`;
1255
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
1256
+ return count.toString();
1257
+ }
1258
+ /**
1259
+ * Template card component inspired by ComfyUI's template showcase
1260
+ * Features:
1261
+ * - Large preview image/screenshot
1262
+ * - Template name and description
1263
+ * - Author info
1264
+ * - Stats (downloads, views, rating)
1265
+ * - Featured badge
1266
+ * - Category/tags
1267
+ */
1268
+ function TemplateCard({ template, onClick, onUseTemplate }) {
1269
+ const tr = useTr();
1270
+ const [imageError, setImageError] = React.useState(false);
1271
+ const preview = templatePreview(template);
1272
+ const previewImage = preview.kind === "image" ? preview.src : void 0;
1273
+ const hasPreview = Boolean(previewImage) && !imageError;
1274
+ const handleUseTemplate = (e) => {
1275
+ e.stopPropagation();
1276
+ onUseTemplate?.(template);
1277
+ };
1278
+ const handleImageError = () => {
1279
+ setImageError(true);
1280
+ };
1281
+ return /* @__PURE__ */ jsxs(Card, {
1282
+ className: cn("group overflow-hidden transition-all duration-300 cursor-pointer", "hover:shadow-xl hover:scale-[1.02] border-2 hover:border-primary/50"),
1283
+ onClick,
1284
+ children: [/* @__PURE__ */ jsxs("div", {
1285
+ className: "relative aspect-video w-full overflow-hidden bg-gradient-to-br from-primary/5 to-primary/10",
1286
+ children: [
1287
+ hasPreview ? /* @__PURE__ */ jsx("img", {
1288
+ src: previewImage,
1289
+ alt: template.name,
1290
+ onError: handleImageError,
1291
+ className: "w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
1292
+ }) : /* @__PURE__ */ jsx("div", {
1293
+ className: "w-full h-full flex items-center justify-center",
1294
+ children: preview.kind === "icon" ? /* @__PURE__ */ jsx(Icon, {
1295
+ icon: preview.name,
1296
+ width: 64,
1297
+ height: 64,
1298
+ className: "text-primary/60",
1299
+ "aria-hidden": "true"
1300
+ }) : /* @__PURE__ */ jsx(Sparkles, { className: "size-16 text-primary/30" })
1301
+ }),
1302
+ template.featured && /* @__PURE__ */ jsx("div", {
1303
+ className: "absolute top-2 right-2",
1304
+ children: /* @__PURE__ */ jsxs(Badge, {
1305
+ variant: "default",
1306
+ className: "bg-status-warning-bg-subtle text-status-warning-text border border-status-warning-border shadow-lg",
1307
+ children: [/* @__PURE__ */ jsx(Star, { className: "size-3 mr-1 fill-current" }), tr("fluidgrids.builder.templateCard.featuredBadge", "Featured")]
1308
+ })
1309
+ }),
1310
+ /* @__PURE__ */ jsx("div", {
1311
+ className: "absolute top-2 left-2",
1312
+ children: template.pricingModel === "FREE" ? /* @__PURE__ */ jsx(Badge, {
1313
+ variant: "secondary",
1314
+ className: "bg-status-success-bg text-action-primary-text border-0",
1315
+ children: tr("fluidgrids.builder.templateCard.freeBadge", "Free")
1316
+ }) : /* @__PURE__ */ jsxs(Badge, {
1317
+ variant: "default",
1318
+ className: "bg-status-info-bg text-action-primary-text border-0",
1319
+ children: ["$", template.price?.toString() || "0"]
1320
+ })
1321
+ }),
1322
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-gradient-to-t from-bg-overlay via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" })
1323
+ ]
1324
+ }), /* @__PURE__ */ jsxs("div", {
1325
+ className: "p-4 space-y-3",
1326
+ children: [
1327
+ /* @__PURE__ */ jsx("h3", {
1328
+ className: "font-semibold text-base line-clamp-1 group-hover:text-primary transition-colors",
1329
+ children: template.name
1330
+ }),
1331
+ /* @__PURE__ */ jsx("p", {
1332
+ className: "text-sm text-muted-foreground line-clamp-2 min-h-[2.5rem]",
1333
+ children: template.description || tr("fluidgrids.builder.templateCard.noDescription", "No description available")
1334
+ }),
1335
+ /* @__PURE__ */ jsxs("div", {
1336
+ className: "flex items-center gap-2 text-xs text-muted-foreground",
1337
+ children: [/* @__PURE__ */ jsx("span", {
1338
+ className: "font-medium",
1339
+ children: template.authorName || template.publisher?.name || tr("fluidgrids.builder.templateCard.unknownAuthor", "Unknown")
1340
+ }), template.publisher?.isVerified && /* @__PURE__ */ jsx(Badge, {
1341
+ variant: "secondary",
1342
+ className: "text-xs px-1.5 py-0",
1343
+ children: "✓"
1344
+ })]
1345
+ }),
1346
+ /* @__PURE__ */ jsxs("div", {
1347
+ className: "flex items-center justify-between text-xs text-muted-foreground pt-2 border-t",
1348
+ children: [/* @__PURE__ */ jsx("div", {
1349
+ className: "flex items-center gap-3",
1350
+ children: template.rating && /* @__PURE__ */ jsxs("div", {
1351
+ className: "flex items-center gap-1",
1352
+ children: [
1353
+ /* @__PURE__ */ jsx(Star, { className: "size-3 fill-status-warning-bg text-status-warning-text" }),
1354
+ /* @__PURE__ */ jsx("span", {
1355
+ className: "font-medium",
1356
+ children: template.rating.toFixed(1)
1357
+ }),
1358
+ /* @__PURE__ */ jsxs("span", {
1359
+ className: "text-muted-foreground/60",
1360
+ children: [
1361
+ "(",
1362
+ template.reviewCount || 0,
1363
+ ")"
1364
+ ]
1365
+ })
1366
+ ]
1367
+ })
1368
+ }), /* @__PURE__ */ jsxs("div", {
1369
+ className: "flex items-center gap-3",
1370
+ children: [/* @__PURE__ */ jsxs("div", {
1371
+ className: "flex items-center gap-1",
1372
+ title: tr("fluidgrids.builder.templateCard.downloadsTooltip", "Downloads"),
1373
+ children: [/* @__PURE__ */ jsx(Download, { className: "size-3" }), /* @__PURE__ */ jsx("span", { children: formatNumber(template.downloads || 0) })]
1374
+ }), /* @__PURE__ */ jsxs("div", {
1375
+ className: "flex items-center gap-1",
1376
+ title: tr("fluidgrids.builder.templateCard.viewsTooltip", "Views"),
1377
+ children: [/* @__PURE__ */ jsx(Eye, { className: "size-3" }), /* @__PURE__ */ jsx("span", { children: formatNumber(template.viewCount || 0) })]
1378
+ })]
1379
+ })]
1380
+ }),
1381
+ (template.category || template.tags?.length > 0) && /* @__PURE__ */ jsxs("div", {
1382
+ className: "flex flex-wrap gap-1",
1383
+ children: [
1384
+ template.category && /* @__PURE__ */ jsx(Badge, {
1385
+ variant: "outline",
1386
+ className: "text-xs",
1387
+ children: template.category
1388
+ }),
1389
+ template.tags?.slice(0, 2).map((tag) => /* @__PURE__ */ jsx(Badge, {
1390
+ variant: "outline",
1391
+ className: "text-xs",
1392
+ children: tag
1393
+ }, tag)),
1394
+ template.tags?.length > 2 && /* @__PURE__ */ jsxs(Badge, {
1395
+ variant: "outline",
1396
+ className: "text-xs",
1397
+ children: ["+", template.tags.length - 2]
1398
+ })
1399
+ ]
1400
+ }),
1401
+ /* @__PURE__ */ jsx(SdkPermissionButton, {
1402
+ permission: FLUIDGRIDS_PERMISSIONS.WORKFLOW_CREATE,
1403
+ deniedTooltip: tr("fluidgrids.templates.useDeniedTooltip", "You don't have permission to create workflows from templates"),
1404
+ children: /* @__PURE__ */ jsxs(Button, {
1405
+ size: "sm",
1406
+ className: "w-full mt-2",
1407
+ onClick: handleUseTemplate,
1408
+ children: [/* @__PURE__ */ jsx(Sparkles, { className: "size-3 mr-1" }), tr("fluidgrids.builder.templateCard.useTemplateButton", "Use Template")]
1409
+ })
1410
+ })
1411
+ ]
1412
+ })]
1413
+ });
1414
+ }
1415
+
1416
+ export { isDurableFunctionNodeType as A, CardFooter as C, RUN_CONTROL_NODE_TYPES as D, DURABLE_FUNCTION_NODE_TYPES as E, isRunControlNodeType as M, WORKFLOW_TEST_NODE_TYPES as O, CardDescription as S, CardTitle as T, storeNodeInstallVersion as _, useTemplateImportFlow as a, Card as b, getWorkflowNodeTypes as c, filterStoreNodesForPalette as d, getStoreNodeAvailability as f, storeNodeDisplayVersion as g, isStoreNodeUpdateAvailable as h, badgeVariants as i, isNodeTypeHiddenFromPalette as j, filterPaletteNodes as k, useImportWorkflowTemplate as l, getStoreNodeType as m, templatePreview as n, buildNodeCheck as o, getStoreNodeIcon as p, Badge as r, buildRequiredNodeTypes as s, TemplateCard as t, useWorkflowTemplates as u, useInstalledNodes as v, CardHeader as w, CardContent as x, useGlobalStoreClient as y };