@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.
- package/README.md +41 -0
- package/dist/AiStudioPage-BHVEU4Wp.js +774 -0
- package/dist/ApprovalInboxPage-dbBDSJ-f.js +558 -0
- package/dist/DataSinkDashboardPage-1h2QsMzH.js +270 -0
- package/dist/DataTablesPage-BZy2z0LB.js +1690 -0
- package/dist/EntityTagPicker-BqRNsLCQ.js +135 -0
- package/dist/{ExecutionDetailPage-D-0_POAf.js → ExecutionDetailPage-BdcZmLT4.js} +799 -4420
- package/dist/ExecutionDetailPage-DvQ667OE.d.ts +10 -0
- package/dist/ExecutionsListPage-BCUJoAIE.js +2742 -0
- package/dist/{FailureTriagePanel-BHiduL7A.js → FailureTriagePanel-B9hDQymi.js} +4 -97
- package/dist/{FluidGridsProvider-DmvFADFa.js → FluidGridsProvider-dvZSNtSW.js} +10 -350
- package/dist/HistoryPage-BLoRpJ1R.d.ts +6 -0
- package/dist/{HistoryPage-BbUgxx1I.js → HistoryPage-U0tqupBN.js} +6 -4
- package/dist/HumanRequestCard-D-ooVPDn.js +439 -0
- package/dist/{InsightsPage-s6f5bU-8.js → InsightsPage-CALo16eB.js} +17 -328
- package/dist/ManagedAiPolicyAuthority-BxwJ3ptd.js +40 -0
- package/dist/{ManagedAiPolicyAuthority-CGux_DaV.d.ts → ManagedAiPolicyAuthority-DM3t3wNr.d.ts} +1 -5
- package/dist/ManagedAiPolicyAuthorityContext-CNkxIOZ1.js +10 -0
- package/dist/{MissingNodesInstallModal-B-9gHy2K.js → MissingNodesInstallModal-CMybsI8h.js} +3 -2
- package/dist/{NodesPage-ClvhUfbh.js → NodesPage-Bf5PhfBK.js} +7 -3
- package/dist/NodesPage-DEbnz9iU.d.ts +6 -0
- package/dist/{PublishCustomNodeAction-D2F7oN0S.js → PublishCustomNodeAction-5XXnSA3H.js} +3 -45
- package/dist/QuotaExhaustedDialog-7IFSaLyi.js +54 -0
- package/dist/RunnerStatusPill-Ctg4TJcP.js +187 -0
- package/dist/RunnersPage-CLCOCclc.js +467 -0
- package/dist/{ManagedAiPolicyAuthority-DsSaz-vg.js → SettingsPage-CJLZvv4J.js} +743 -773
- package/dist/SettingsPage-DMHQFj5-.d.ts +9 -0
- package/dist/TagFilterSelect-DCBOH6Ou.js +41 -0
- package/dist/TaskQueuesPage-9VgSnLdL.js +951 -0
- package/dist/TemplateCard-DsC0KNH1.js +1416 -0
- package/dist/{TemplateConfigurationModal-D_Mvgxqa.js → TemplateConfigurationModal-BhGWIzpm.js} +3 -2
- package/dist/{TemplateDetailPanel-DfFfUcOe.js → TemplateDetailPanel-D4cUVr-m.js} +3 -3
- package/dist/TemplatePreview-FayF6jxh.js +231 -0
- package/dist/TemplatesPage-BG3H-6v6.d.ts +10 -0
- package/dist/{TemplatesPage-Cku13bav.js → TemplatesPage-CsDNT54N.js} +11 -228
- package/dist/VariablesPage-67WR_lVE.js +949 -0
- package/dist/{VersionDiffDialog-Dq2xlPFI.js → VersionDiffDialog-DSN0mU0f.js} +2 -1
- package/dist/WorkflowBuilder-CPnXEL5J.js +555 -0
- package/dist/{WorkflowSearchRedirect-CaoOOpEl.js → WorkflowBuilder-YXRysvZc.js} +19 -585
- package/dist/{WorkflowProposalCard-8Vr56kQa.js → WorkflowProposalCard-qrEeNyYG.js} +4 -405
- package/dist/WorkflowSearchRedirect-DwDNZ_q5.js +36 -0
- package/dist/WorkflowShareDialog-mbIwRQUY.js +168 -0
- package/dist/WorkflowsList-CcNDQge5.d.ts +6 -0
- package/dist/{WorkflowsList-B2j1Pehb.js → WorkflowsList-DiiGyZkT.js} +12 -6
- package/dist/agent-form-Bfvm32j1.js +83 -0
- package/dist/{agentTools-DJ5o2ekg.js → agentTools-8s_25Q_F.js} +3 -2
- package/dist/{ai-usage-D5p7ZeRR.js → ai-usage-glCwHyQz.js} +1 -1
- package/dist/api-BrCHB5oA.js +224 -0
- package/dist/api-C8e0iV-Q.js +55 -0
- package/dist/button-DG7y_FVJ.js +48 -0
- package/dist/{canvasNoteTools-D3zUdEeg.js → canvasNoteTools-B6pYARgU.js} +2 -2
- package/dist/client-C9XSzSXr.js +1147 -0
- package/dist/code-editor-field-9-GdVE_8.js +111 -0
- package/dist/{codec-client-DstqyzTN.js → codec-client-CJ95Rmqo.js} +1 -1
- package/dist/constants-B2lhWNoN.js +202 -0
- package/dist/constants-XdvUxeWK.d.ts +162 -0
- package/dist/dialog-Cjb4lB4D.js +105 -0
- package/dist/executions.d.ts +1 -1
- package/dist/executions.js +2 -1
- package/dist/feature-flags-DM5myWaD.js +296 -0
- package/dist/flags.d.ts +3 -0
- package/dist/flags.js +4 -0
- package/dist/{flow-builder-DEzT8TQu.js → flow-builder-CZDKRc6f.js} +662 -10716
- package/dist/{node-endpoint-configs-DHFGA5MQ.js → graphql-operation-types-kX-MMjgW.js} +1 -1194
- package/dist/{graphql-types-XcDLtFwU.js → graphql-types-CDKVWH2R.js} +2 -202
- package/dist/history.d.ts +1 -1
- package/dist/history.js +1 -1
- package/dist/{index-DRf25-D3.d.ts → index-6JR2vD9J.d.ts} +33 -215
- package/dist/index-72XFgvbn.d.ts +132 -0
- package/dist/index-BcdtKBM-.d.ts +56 -0
- package/dist/{index-B3BWqafN.d.ts → index-DjiFgEQl.d.ts} +5 -131
- package/dist/index.d.ts +13 -9
- package/dist/index.js +52 -5300
- package/dist/{node-detail-view-LNqeIqN-.js → node-detail-view-CV4ZRwqh.js} +13 -8
- package/dist/node-editor-tab-body-DU-dh35X.js +174 -0
- package/dist/node-endpoint-configs-B5bjNMDW.js +37 -0
- package/dist/node-registry-refresh-Dy0uRg9f.js +84 -0
- package/dist/nodes.d.ts +1 -1
- package/dist/nodes.js +1 -1
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +2 -1
- package/dist/popover-BJHvrFGR.js +472 -0
- package/dist/{quick-config-panel-D3ogsaFL.js → quick-config-panel-ws3pLvbt.js} +3 -2
- package/dist/rolldown-runtime-hDbaFkMy.js +18 -0
- package/dist/run-display-BycKYSU0.js +46 -0
- package/dist/run-list-links-ByYC0OlV.js +98 -0
- package/dist/schedule-panel-C0mfGQSD.js +7542 -0
- package/dist/settings.d.ts +2 -1
- package/dist/settings.js +2 -1
- package/dist/shared-CMfn30xT.js +196 -0
- package/dist/sidebar-node-editor-CR1PiZO-.js +177 -0
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +2 -1
- package/dist/theme-provider-B3mIuz2d.js +11 -0
- package/dist/{useAgentDefinitions-NFtLytDR.js → useAgentDefinitions-B0dam3_o.js} +3 -397
- package/dist/useConnections-Cs7LCWEM.js +401 -0
- package/dist/useDurableParity-CzUQMFJ3.js +415 -0
- package/dist/useEntityTags-DhnsZcTl.js +236 -0
- package/dist/{useExecutions-DAzeWHS6.js → useExecutions-9aLuOt1c.js} +4 -3
- package/dist/{useObservability-COCZ0hUS.js → useObservability-CfEw3ZFD.js} +2 -2
- package/dist/{useRunHistory-CsYUbouO.js → useRunHistory-B1xJDwl1.js} +4 -3
- package/dist/{WorkflowShareDialog-6RRDe-_v.js → useWorkflowCodeExport-Bu2QYJzA.js} +7 -210
- package/dist/{useWorkflows-w4Dx2fF2.js → useWorkflows-BlVVu8NM.js} +3 -2
- package/dist/utils-uaL5UVxb.js +65 -0
- package/dist/workflow-builder.d.ts +1 -1
- package/dist/workflow-builder.js +4 -2
- package/dist/workflow-host.d.ts +6 -0
- package/dist/workflow-host.js +9 -0
- package/dist/workflow-pages.d.ts +55 -0
- package/dist/workflow-pages.js +113 -0
- package/dist/workflows-list.d.ts +1 -1
- package/dist/workflows-list.js +1 -1
- package/package.json +13 -1
- package/dist/ExecutionDetailPage-MvsnOnE1.d.ts +0 -4
- package/dist/HistoryPage-BqUizaFv.d.ts +0 -3
- package/dist/NodesPage-C97-9K2Y.d.ts +0 -3
- package/dist/TemplatesPage-D0qnXzHh.d.ts +0 -4
- package/dist/WorkflowsList-0d5GSKdh.d.ts +0 -3
- 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 };
|