@burdenoff/fluidgrids-fe-sdk 2026.1111.11 → 2026.1111.13

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 (45) hide show
  1. package/dist/{ExecutionDetailPage-DFjVF9B6.js → ExecutionDetailPage-CDkJeeu2.js} +9 -10
  2. package/dist/{FailureTriagePanel-CR1BzOrM.js → FailureTriagePanel-BHiduL7A.js} +1 -1
  3. package/dist/{FluidGridsProvider-DpTTWDNQ.js → FluidGridsProvider-CDZjKmfD.js} +1 -2
  4. package/dist/{HistoryPage-CfefCOUa.js → HistoryPage-DnVGN_72.js} +3 -3
  5. package/dist/{InsightsPage-Cbf0cdbC.js → InsightsPage-CGRpZtGu.js} +4 -4
  6. package/dist/{ManagedAiPolicyAuthority-BO67oJlr.js → ManagedAiPolicyAuthority-DbJBledZ.js} +6 -6
  7. package/dist/{MissingNodesInstallModal-DuOtLV6V.js → MissingNodesInstallModal-Ca4f--lw.js} +2 -2
  8. package/dist/{NodesPage-C4KtAasX.js → NodesPage-ClvhUfbh.js} +2 -2
  9. package/dist/{PublishCustomNodeAction-C3fEmWnC.js → PublishCustomNodeAction-D2F7oN0S.js} +1 -1
  10. package/dist/{TemplateConfigurationModal-DwJi6alN.js → TemplateConfigurationModal-ChQsXf0-.js} +2 -2
  11. package/dist/{TemplateDetailPanel-Cpzjif1n.js → TemplateDetailPanel-C0YiVdub.js} +2 -2
  12. package/dist/{TemplatesPage-B3fjphKN.js → TemplatesPage-QXtq6mI3.js} +113 -504
  13. package/dist/{VersionDiffDialog-ExN-CN6f.js → VersionDiffDialog-Dq2xlPFI.js} +1 -1
  14. package/dist/{WorkflowProposalCard-DuXQI7UY.js → WorkflowProposalCard-8Vr56kQa.js} +1 -1
  15. package/dist/{WorkflowSearchRedirect-D4JvOvoT.js → WorkflowSearchRedirect-B4nHALJg.js} +10 -11
  16. package/dist/{WorkflowShareDialog-ByCefHCP.js → WorkflowShareDialog-ByTSWwwR.js} +2 -2
  17. package/dist/{WorkflowsList-XPY9rJpX.js → WorkflowsList-B9F2e8M6.js} +5 -6
  18. package/dist/{agent-form-CzG3vX_q.js → agent-form-BvMWhQ42.js} +1 -1
  19. package/dist/{agentTools-ZSPi5eH3.js → agentTools-C0g2OWvI.js} +2 -2
  20. package/dist/canvasNoteTools-BUWY18XK.js +136 -0
  21. package/dist/{codec-client-DiFkc0yn.js → codec-client-DstqyzTN.js} +1 -1
  22. package/dist/executions.js +1 -1
  23. package/dist/{flow-builder-BjF8QGp5.js → flow-builder-BvCQAGDR.js} +1552 -785
  24. package/dist/history.js +1 -1
  25. package/dist/{index-BES5GtJJ.d.ts → index-CLqh2u28.d.ts} +73 -2
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +447 -26
  28. package/dist/{node-detail-view-Cfcpoa28.js → node-detail-view-CPD_NSZc.js} +6 -6
  29. package/dist/{node-endpoint-configs-DVo9nUOe.js → node-endpoint-configs-DHFGA5MQ.js} +17 -3
  30. package/dist/nodes.js +1 -1
  31. package/dist/observability.d.ts +1 -1
  32. package/dist/observability.js +1 -1
  33. package/dist/{quick-config-panel-DSEO6BGC.js → quick-config-panel-DHkYGaCL.js} +2 -2
  34. package/dist/settings.js +1 -1
  35. package/dist/templates.js +1 -1
  36. package/dist/{useAgentDefinitions-BVTONKpB.js → useAgentDefinitions-NFtLytDR.js} +1 -1
  37. package/dist/{useExecutions-C84_O9Qv.js → useExecutions-BqoD7XWi.js} +2 -2
  38. package/dist/{useObservability--XT0i9Lc.js → useObservability-Cs-5_NHh.js} +2 -2
  39. package/dist/{useRunHistory-B0IcKx_P.js → useRunHistory-CEL-CIz-.js} +2 -2
  40. package/dist/{useWorkflows-CDNmiA8o.js → useWorkflows-Bwpvfdgv.js} +2 -2
  41. package/dist/workflow-builder.js +2 -2
  42. package/dist/workflows-list.js +1 -1
  43. package/package.json +1 -1
  44. package/dist/rolldown-runtime-hDbaFkMy.js +0 -18
  45. package/dist/template-input-applicator-C0p6hE4K.js +0 -84
@@ -1,27 +1,27 @@
1
1
  "use client";
2
- import { t as __exportAll } from "./rolldown-runtime-hDbaFkMy.js";
3
2
  import { A as nodeAccent, B as NODE_TYPE_DRAG_DATA_FORMAT, G as isTriggerOperation, H as useFlowStore, M as isImageIcon, T as resolveInputHandles, U as useIsValidConnection, V as STICKY_NOTE_TYPE, g as toCodeMirrorDiagnostics, j as NodeTypeIcon, r as CODE_WORKFLOW_NODE_TYPE, z as NODE_OPERATION_DRAG_DATA_FORMAT } from "./node-operation-selection--oy-yEq_.js";
4
- import { $ as resolveLoadOptionsSpec, $n as useWorkflowTestsEnabled, $t as useNodeExecution, A as UninstallWorkflowNodeDocument, At as bannerOf, B as coerceNumber, Bn as FLUIDGRIDS_FLAGS, Bt as forgetSessionForReload, C as InstallOrganizationPrivateNodeDocument, Cn as RUNS_QUOTA_NAME, D as OrganizationPrivateNodesDocument, Dt as useSaveStatusStore, Et as saveIndicatorOf, F as useInsertNode, Ft as phaseAfterTakeOver, G as paramsFromOperation, Gt as AUTOSAVE_DEBOUNCE_MS, H as nodeSubtitle, Ht as sessionBeforeReload, I as MissingParametersDialog, It as releasesOnClose, Jn as useFluidGridsFlag, Kn as useDurableFunctionNodesEnabled, Kt as AUTOSAVE_RETRY_DELAYS_MS, L as useRunGuard, Lt as shouldReleaseForIdle, M as getNodeMetadata, Mt as leaseCallQueue, N as buildOperationSeed, Nt as leaseCallSequence, On as useCanvasEdges, Ot as useEditLeaseLocked, Pt as phaseAfterAcquire, Q as optionListValues, Qt as workflowFingerprint, R as workflowParameterIssues, Rt as tickAction, S as GetNodeInstallableVersionsDocument, Sn as BILLING_PLANS_PATH, T as InstallWorkflowNodeDocument, Tn as collectExtensions, Tt as refusalOfQuota, U as nodeTitle, Ut as rememberOwnStatusChange, Vt as rememberSessionForReload, W as optionText, Wt as rememberRevision, X as isStringOptionList, Xn as usePaletteGates, Xt as inAppLeaveTarget, Z as optionListText, Zn as usePhaseCFlags, Zt as saveFailureActionOf, _n as emitWorkflowCreated, _t as watchLeavingLosesChanges, a as NODES_METADATA, an as structuredParameterType, ar as getNodeDetail, b as CreateStoreOrderDocument, bt as useSaveWorkflow, c as ensureNodeConfigsLoaded, cn as isParamVisible, cr as useBuilderReadOnly, ct as useGuardedNavigate, d as onNodeTypesUpdate, dn as emitNodeInstalled, dt as departingSave, er as connectionsForAdapter, et as ConditionTreeBuilder, f as refetchStoreNodes, ft as editStateFor, g as scheduleNodePaletteLoad, gn as emitUpgradeClicked, gt as touchesSavedFields, hn as emitTemplateUsed, ht as savedEverythingBeforeLeaving, i as AVAILABLE_NODES, in as parseParameterValue, ir as replaceAdapterConnections, it as parseMappingValue, jn as loadDefinitionJson, jt as isLocked, k as UninstallOrganizationPrivateNodeDocument, kn as useNodeRegistryRevision, kt as useEditLeaseStore, l as ensureNodeTypesLoaded, ln as useFluidGridsAnalytics, lt as useManualSave, m as registerWorkflowNodeTypes, mt as leavingLosesChanges, nr as matchesAdapterProvider, nt as casesFromRules, o as NODE_CONFIGS, or as buildStickyNode, ot as boundFieldsOf, pt as editingLeaseLocked, q as useNodeConfig, qn as useDurableParityEnabled, qt as canAutosave, rn as formatParameterValue, rr as newConnectionProviderKey, rt as FieldTree, s as NODE_TYPES, sr as BuilderReadOnlyProvider, st as missingRequiredParameters, t as FluidGridsContext, tr as isOptionalAdapter, tt as SwitchRulesEditor, u as isNodeConfigsInitialized, un as openRunQuotaUpgradePrompt, ur as getGlobalStoreClient, ut as autosaveAllowedFor, v as BrowseWorkflowNodesDocument, vt as isFlowComplete, w as InstallPurchasedNodeDocument, x as GetMyNodeInstallationsDocument, y as BrowseWorkflowTemplatesDocument, yt as useFlowValidator, z as asRecord, zt as EDIT_SESSION_ID } from "./FluidGridsProvider-DpTTWDNQ.js";
3
+ import { $ as resolveLoadOptionsSpec, $n as useWorkflowTestsEnabled, $t as useNodeExecution, A as UninstallWorkflowNodeDocument, At as bannerOf, B as coerceNumber, Bn as FLUIDGRIDS_FLAGS, Bt as forgetSessionForReload, C as InstallOrganizationPrivateNodeDocument, Cn as RUNS_QUOTA_NAME, D as OrganizationPrivateNodesDocument, Dn as isQuotaExhaustedFailure, Dt as useSaveStatusStore, Et as saveIndicatorOf, F as useInsertNode, Ft as phaseAfterTakeOver, G as paramsFromOperation, Gt as AUTOSAVE_DEBOUNCE_MS, H as nodeSubtitle, Ht as sessionBeforeReload, I as MissingParametersDialog, It as releasesOnClose, Jn as useFluidGridsFlag, Kn as useDurableFunctionNodesEnabled, Kt as AUTOSAVE_RETRY_DELAYS_MS, L as useRunGuard, Lt as shouldReleaseForIdle, M as getNodeMetadata, Mt as leaseCallQueue, N as buildOperationSeed, Nt as leaseCallSequence, On as useCanvasEdges, Ot as useEditLeaseLocked, Pt as phaseAfterAcquire, Q as optionListValues, Qt as workflowFingerprint, R as workflowParameterIssues, Rt as tickAction, S as GetNodeInstallableVersionsDocument, Sn as BILLING_PLANS_PATH, T as InstallWorkflowNodeDocument, Tn as collectExtensions, Tt as refusalOfQuota, U as nodeTitle, Ut as rememberOwnStatusChange, Vt as rememberSessionForReload, W as optionText, Wt as rememberRevision, X as isStringOptionList, Xn as usePaletteGates, Xt as inAppLeaveTarget, Z as optionListText, Zn as usePhaseCFlags, Zt as saveFailureActionOf, _n as emitWorkflowCreated, _t as watchLeavingLosesChanges, a as NODES_METADATA, an as structuredParameterType, ar as getNodeDetail, b as CreateStoreOrderDocument, bt as useSaveWorkflow, c as ensureNodeConfigsLoaded, cn as isParamVisible, cr as useBuilderReadOnly, ct as useGuardedNavigate, d as onNodeTypesUpdate, dn as emitNodeInstalled, dt as departingSave, er as connectionsForAdapter, et as ConditionTreeBuilder, f as refetchStoreNodes, ft as editStateFor, g as scheduleNodePaletteLoad, gn as emitUpgradeClicked, gt as touchesSavedFields, hn as emitTemplateUsed, ht as savedEverythingBeforeLeaving, i as AVAILABLE_NODES, in as parseParameterValue, ir as replaceAdapterConnections, it as parseMappingValue, jn as loadDefinitionJson, jt as isLocked, k as UninstallOrganizationPrivateNodeDocument, kn as useNodeRegistryRevision, kt as useEditLeaseStore, l as ensureNodeTypesLoaded, ln as useFluidGridsAnalytics, lt as useManualSave, m as registerWorkflowNodeTypes, mt as leavingLosesChanges, nr as matchesAdapterProvider, nt as casesFromRules, o as NODE_CONFIGS, or as buildStickyNode, ot as boundFieldsOf, pt as editingLeaseLocked, q as useNodeConfig, qn as useDurableParityEnabled, qt as canAutosave, rn as formatParameterValue, rr as newConnectionProviderKey, rt as FieldTree, s as NODE_TYPES, sr as BuilderReadOnlyProvider, st as missingRequiredParameters, t as FluidGridsContext, tr as isOptionalAdapter, tt as SwitchRulesEditor, u as isNodeConfigsInitialized, un as openRunQuotaUpgradePrompt, ur as getGlobalStoreClient, ut as autosaveAllowedFor, v as BrowseWorkflowNodesDocument, vt as isFlowComplete, w as InstallPurchasedNodeDocument, x as GetMyNodeInstallationsDocument, y as BrowseWorkflowTemplatesDocument, yt as useFlowValidator, z as asRecord, zt as EDIT_SESSION_ID } from "./FluidGridsProvider-CDZjKmfD.js";
5
4
  import { r as useTr } from "./TranslationBridge-Da-TPgQm.js";
6
- import { Ai as getFluidGridsClientSafe, Br as WorkflowGitRepositoryDocument, Ct as LinkWorkflowGitRepositoryDocument, Dr as WorkflowDefinitionDocument, Ei as WorkspaceRunsQuotaDocument, Ht as PreviewScheduleDocument, Ir as WorkflowGenerationTemplatesDocument, J as DeleteWorkflowRolloutDocument, Ji as getFluidGridsConfig, Kt as PushWorkflowToGitDocument, Lr as WorkflowGitCommitsDocument, Mt as NodeSchemaIntrospectionDocument, Nr as WorkflowEnvironmentsDocument, Pi as waitForFluidGridsClient, Pt as NodeUsageStatusDocument, Qt as RemoveNodeFromWorkflowsDocument, Ri as emitFluidGridsEvent, Rr as WorkflowGitDiffDocument, Si as WorkflowVersioningDocument, Vi as describeOperationError, Vn as TakeOverWorkflowEditLeaseDocument, Wi as throwIfResultError, Wr as WorkflowPromotionRequestsDocument, Xn as UnlinkWorkflowGitRepositoryDocument, Yt as RejectWorkflowPromotionDocument, Z as DeployWorkflowVersionToDevDocument, Zt as ReleaseWorkflowEditLeaseDocument, cr as UpdateWorkflowEnvironmentPolicyDocument, i as ActivateWorkflowVersionCheckedDocument, jn as SetWorkflowRolloutDocument, jr as WorkflowEditLeaseDocument, ki as getFluidGridsClient, mr as WebhookTestListenerDocument, n as AcquireWorkflowEditLeaseDocument, nn as RequestWorkflowPromotionDocument, on as RestoreWorkflowFromGitDocument, u as ApproveWorkflowPromotionDocument, wr as WorkflowCallInputsDocument, wt as ListenForWebhookTestEventDocument, x as CancelWorkflowPromotionDocument, xi as WorkflowVersionDrainageDocument, yr as WorkflowAuthoringDocument, zn as StopWebhookTestListenerDocument, zr as WorkflowGitPushesDocument } from "./node-endpoint-configs-DVo9nUOe.js";
7
- import { D as describeSchedule, H as pauseSchedule, _ as useResource, c as FieldError, d as NativeSelect, et as triggerSchedule, nt as unpauseSchedule, p as StatusPill, r as describeRule, s as isAgentNodeType, v as backfillSchedule } from "./agent-form-CzG3vX_q.js";
5
+ import { Bi as describeOperationError, Br as WorkflowGitRepositoryDocument, Ct as LinkWorkflowGitRepositoryDocument, Dr as WorkflowDefinitionDocument, Ei as WorkspaceRunsQuotaDocument, Ht as PreviewScheduleDocument, Ir as WorkflowGenerationTemplatesDocument, J as DeleteWorkflowRolloutDocument, Kt as PushWorkflowToGitDocument, Li as emitFluidGridsEvent, Lr as WorkflowGitCommitsDocument, Mt as NodeSchemaIntrospectionDocument, Ni as waitForFluidGridsClient, Nr as WorkflowEnvironmentsDocument, Oi as getFluidGridsClient, Pt as NodeUsageStatusDocument, Qt as RemoveNodeFromWorkflowsDocument, Rr as WorkflowGitDiffDocument, Si as WorkflowVersioningDocument, Ui as throwIfResultError, Vn as TakeOverWorkflowEditLeaseDocument, Wr as WorkflowPromotionRequestsDocument, Xn as UnlinkWorkflowGitRepositoryDocument, Yt as RejectWorkflowPromotionDocument, Z as DeployWorkflowVersionToDevDocument, Zt as ReleaseWorkflowEditLeaseDocument, cr as UpdateWorkflowEnvironmentPolicyDocument, i as ActivateWorkflowVersionCheckedDocument, jn as SetWorkflowRolloutDocument, jr as WorkflowEditLeaseDocument, ki as getFluidGridsClientSafe, mr as WebhookTestListenerDocument, n as AcquireWorkflowEditLeaseDocument, nn as RequestWorkflowPromotionDocument, oa as __exportAll, on as RestoreWorkflowFromGitDocument, qi as getFluidGridsConfig, u as ApproveWorkflowPromotionDocument, wr as WorkflowCallInputsDocument, wt as ListenForWebhookTestEventDocument, x as CancelWorkflowPromotionDocument, xi as WorkflowVersionDrainageDocument, yr as WorkflowAuthoringDocument, zn as StopWebhookTestListenerDocument, zr as WorkflowGitPushesDocument } from "./node-endpoint-configs-DHFGA5MQ.js";
6
+ import { D as describeSchedule, H as pauseSchedule, _ as useResource, c as FieldError, d as NativeSelect, et as triggerSchedule, nt as unpauseSchedule, p as StatusPill, r as describeRule, s as isAgentNodeType, v as backfillSchedule } from "./agent-form-BvMWhQ42.js";
8
7
  import { CF as WorkflowExportFormat, Eo as CodeReplaceStatus, PD as ScheduleType, QF as WorkflowTestExpectedStatus, SF as WorkflowEnvironment, VF as WorkflowScheduleOverlapPolicy, hI as FLUIDGRIDS_PERMISSIONS, vI as SdkPermissionButton, xI as useFluidGridsPermission, xu as DrainageStatus, yI as SdkPermissionGate, yP as VersioningBehavior } from "./graphql-types-XcDLtFwU.js";
9
- import { a as useConnections, r as useAgentsUsingConnection, t as useAgentDefinitionList, wt as useRunFromNodeEligibilityQuery } from "./useAgentDefinitions-BVTONKpB.js";
10
- import { F as normaliseAssertRules, I as operatorSpec, L as readAssertRulesSetting, M as ASSERT_OPERATORS, N as describeAssertRuleProblem, P as emptyAssertRuleRow, R as rowsFromRules, _ as buildNodeDisplayMap, a as TEST_STATUS_TONE_CLASS, b as EntityTagPicker, c as draftToInput, d as isTestRunOpen, f as nextSortOrder, g as testStatusTone, h as testStatusLabel, i as useWorkflowTests, l as emptyTestCaseDraft, m as testRunSummaryText, n as useWorkflowProposal, p as pinnedOutputsFromCanvas, s as draftFromTestCase, t as WorkflowProposalCard, u as enabledCaseIds, z as ruleFromRow } from "./WorkflowProposalCard-DuXQI7UY.js";
8
+ import { a as useConnections, r as useAgentsUsingConnection, t as useAgentDefinitionList, wt as useRunFromNodeEligibilityQuery } from "./useAgentDefinitions-NFtLytDR.js";
9
+ import { F as normaliseAssertRules, I as operatorSpec, L as readAssertRulesSetting, M as ASSERT_OPERATORS, N as describeAssertRuleProblem, P as emptyAssertRuleRow, R as rowsFromRules, _ as buildNodeDisplayMap, a as TEST_STATUS_TONE_CLASS, b as EntityTagPicker, c as draftToInput, d as isTestRunOpen, f as nextSortOrder, g as testStatusTone, h as testStatusLabel, i as useWorkflowTests, l as emptyTestCaseDraft, m as testRunSummaryText, n as useWorkflowProposal, p as pinnedOutputsFromCanvas, s as draftFromTestCase, t as WorkflowProposalCard, u as enabledCaseIds, z as ruleFromRow } from "./WorkflowProposalCard-8Vr56kQa.js";
11
10
  import { t as cn$1 } from "./cn-QqidFIgr.js";
12
- import { i as Button$2, t as PublishCustomNodeAction } from "./PublishCustomNodeAction-C3fEmWnC.js";
13
- import { O as CreateWorkflowRefusalDialog, b as lineOfNode, h as fileBaseName, i as useWorkflowCodeExport, n as downloadWorkflowCodeExport, t as WorkflowShareDialog, v as isLosslessReport, w as sortFidelityItems, x as orderedExportFiles, y as isLossyFidelityItem } from "./WorkflowShareDialog-ByCefHCP.js";
11
+ import { i as Button$2, t as PublishCustomNodeAction } from "./PublishCustomNodeAction-D2F7oN0S.js";
12
+ import { O as CreateWorkflowRefusalDialog, b as lineOfNode, h as fileBaseName, i as useWorkflowCodeExport, n as downloadWorkflowCodeExport, t as WorkflowShareDialog, v as isLosslessReport, w as sortFidelityItems, x as orderedExportFiles, y as isLossyFidelityItem } from "./WorkflowShareDialog-ByTSWwwR.js";
14
13
  import { t as FluidGridsDocsLink } from "./FluidGridsDocsLink-JxkBpOeb.js";
15
- import { a as useDeactivateWorkflow, d as useRestoreWorkflowVersion, o as useDeleteWorkflow, t as useActivateWorkflow, v as useWorkflowVersions } from "./useWorkflows-CDNmiA8o.js";
16
- import { n as VersionLifecycleBar, r as countLifecycle, t as VersionDiffDialog } from "./VersionDiffDialog-ExN-CN6f.js";
14
+ import { a as useDeactivateWorkflow, d as useRestoreWorkflowVersion, o as useDeleteWorkflow, t as useActivateWorkflow, v as useWorkflowVersions } from "./useWorkflows-Bwpvfdgv.js";
15
+ import { n as VersionLifecycleBar, r as countLifecycle, t as VersionDiffDialog } from "./VersionDiffDialog-Dq2xlPFI.js";
17
16
  import { n as useRequestedProposal } from "./useRequestedProposal-B7uPXfZB.js";
18
17
  import { t as AiCreditBalanceSlot } from "./aiCreditsBridge-BuE92tKo.js";
19
18
  import * as React$1 from "react";
20
19
  import React, { Suspense, createContext, forwardRef, lazy, memo, useCallback, useContext, useDeferredValue, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
21
20
  import { Link, useNavigate, useParams } from "react-router";
22
21
  import { useMutation, useQuery } from "@apollo/client/react";
22
+ import { gql } from "@apollo/client/core";
23
23
  import { create } from "zustand";
24
- import { Background, Controls, EdgeLabelRenderer, Handle, MarkerType, MiniMap, Panel, Position, ReactFlow, ReactFlowProvider, addEdge, getBezierPath, useEdgesState, useNodes, useNodesState, useOnSelectionChange, useReactFlow, useStore } from "@xyflow/react";
24
+ import { Background, Controls, EdgeLabelRenderer, Handle, MarkerType, MiniMap, NodeToolbar, Panel, Position, ReactFlow, ReactFlowProvider, ViewportPortal, addEdge, getBezierPath, useEdgesState, useInternalNode, useNodes, useNodesState, useOnSelectionChange, useReactFlow, useStore } from "@xyflow/react";
25
25
  import { Icon } from "@iconify/react";
26
26
  import ReactMarkdown from "react-markdown";
27
27
  import { cn } from "@burdenoff/fe-libs/utils";
@@ -30,7 +30,7 @@ import { produce } from "immer";
30
30
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
31
31
  import { toast } from "sonner";
32
32
  import { edgeColor, nodeCategoryColor } from "@burdenoff/fe-libs/shared/utils";
33
- import { gql } from "@apollo/client";
33
+ import { gql as gql$1 } from "@apollo/client";
34
34
  import { shallow, useShallow } from "zustand/shallow";
35
35
  import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Input, Label, Popover, PopoverContent, PopoverTrigger, ResponsiveDialog, ResponsiveDialogContent, ResponsiveDialogDescription, ResponsiveDialogFooter, ResponsiveDialogHeader, ResponsiveDialogTitle, ScrollArea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, Toaster as Toaster$1, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@burdenoff/fe-libs/ui";
36
36
  import { useEventBus } from "@burdenoff/fe-libs/shared/events";
@@ -2028,7 +2028,7 @@ function WebhookTestListenerControls({ nodeId }) {
2028
2028
  }
2029
2029
 
2030
2030
  // src/builder/components/ndv/workflow-picker-field.tsx
2031
- const LIST_WORKFLOWS_QUERY = gql`
2031
+ const LIST_WORKFLOWS_QUERY = gql$1`
2032
2032
  query ListWorkflowDefinitionsForPicker($limit: Int, $offset: Int) {
2033
2033
  workflowDefinitions(input: { limit: $limit, offset: $offset }) {
2034
2034
  items {
@@ -2462,7 +2462,7 @@ function InputField({ field, mapping, inputMapping, onChange, testId }) {
2462
2462
  }
2463
2463
 
2464
2464
  // src/hooks/load-node-options-request.ts
2465
- const LOAD_NODE_OPTIONS = gql`
2465
+ const LOAD_NODE_OPTIONS = gql$1`
2466
2466
  query LoadNodeOptions(
2467
2467
  $nodeType: String!
2468
2468
  $nodeVersion: String!
@@ -3544,7 +3544,7 @@ function defaultScheduleRule() {
3544
3544
  function isScheduleRuleField(value) {
3545
3545
  return typeof value === "string" && SCHEDULE_RULE_FIELDS.some((field) => field === value);
3546
3546
  }
3547
- function isRecord$5(value) {
3547
+ function isRecord$6(value) {
3548
3548
  return typeof value === "object" && value !== null && !Array.isArray(value);
3549
3549
  }
3550
3550
  function toNumber(value) {
@@ -3643,10 +3643,10 @@ function rulesFromLegacyState(state) {
3643
3643
  * array and the legacy fields, and always yields at least one rule.
3644
3644
  */
3645
3645
  function normalizeScheduleState(state) {
3646
- const source = isRecord$5(state) ? state : {};
3646
+ const source = isRecord$6(state) ? state : {};
3647
3647
  let rules;
3648
3648
  if (Array.isArray(source.rules)) {
3649
- rules = source.rules.filter(isRecord$5).map(normalizeScheduleRule);
3649
+ rules = source.rules.filter(isRecord$6).map(normalizeScheduleRule);
3650
3650
  if (rules.length === 0) rules = [defaultScheduleRule()];
3651
3651
  } else rules = rulesFromLegacyState(source);
3652
3652
  const timezone = toTrimmedString(source.timezone) || "UTC";
@@ -3661,7 +3661,7 @@ function normalizeScheduleState(state) {
3661
3661
  }
3662
3662
  /** Whether the saved state predates `rules[]` (so the editor is showing a conversion). */
3663
3663
  function isLegacyScheduleState(state) {
3664
- if (!isRecord$5(state) || Array.isArray(state.rules)) return false;
3664
+ if (!isRecord$6(state) || Array.isArray(state.rules)) return false;
3665
3665
  return state.mode !== void 0 || toTrimmedString(state.cronExpression) !== "" || toNumber(state.interval) !== void 0;
3666
3666
  }
3667
3667
  function inRange(value, min, max) {
@@ -4311,7 +4311,7 @@ const TOOL_APPROVAL_RULES = [
4311
4311
  function isRule(value) {
4312
4312
  return typeof value === "string" && TOOL_APPROVAL_RULES.includes(value);
4313
4313
  }
4314
- function isRecord$4(value) {
4314
+ function isRecord$5(value) {
4315
4315
  return typeof value === "object" && value !== null && !Array.isArray(value);
4316
4316
  }
4317
4317
  function parsed(value) {
@@ -4325,11 +4325,11 @@ function parsed(value) {
4325
4325
  /** The rows of a stored setting; rows with an unknown rule read as `ask`. */
4326
4326
  function readToolApprovalRows(value) {
4327
4327
  const raw = parsed(value);
4328
- if (Array.isArray(raw)) return raw.filter(isRecord$4).map((row) => ({
4328
+ if (Array.isArray(raw)) return raw.filter(isRecord$5).map((row) => ({
4329
4329
  toolName: typeof row.toolName === "string" ? row.toolName : "",
4330
4330
  rule: isRule(row.rule) ? row.rule : "ask"
4331
4331
  }));
4332
- if (isRecord$4(raw)) return Object.entries(raw).map(([toolName, rule]) => ({
4332
+ if (isRecord$5(raw)) return Object.entries(raw).map(([toolName, rule]) => ({
4333
4333
  toolName,
4334
4334
  rule: isRule(rule) ? rule : "ask"
4335
4335
  }));
@@ -4346,9 +4346,9 @@ function connectedToolNames(agentId, nodes, edges) {
4346
4346
  for (const edge of edges) {
4347
4347
  if (edge.source !== agentId || edge.sourceHandle !== "Tool") continue;
4348
4348
  const node = edge.target ? byId.get(edge.target) : void 0;
4349
- const args = isRecord$4(node?.data) ? node.data.args : void 0;
4350
- const state = isRecord$4(args) ? args.state : void 0;
4351
- const name = isRecord$4(state) ? state.toolName : void 0;
4349
+ const args = isRecord$5(node?.data) ? node.data.args : void 0;
4350
+ const state = isRecord$5(args) ? args.state : void 0;
4351
+ const name = isRecord$5(state) ? state.toolName : void 0;
4352
4352
  if (typeof name === "string" && name.trim()) names.add(name.trim());
4353
4353
  }
4354
4354
  return [...names];
@@ -4921,7 +4921,7 @@ function TimezoneSelect({ value, onChange, className, testId }) {
4921
4921
  /** Builder field types the trigger manifests give the two picker fields. */
4922
4922
  const PLATFORM_EVENT_MODULE_FIELD = "platform-event-module";
4923
4923
  const PLATFORM_EVENT_RECORD_TYPE_FIELD = "platform-event-record-type";
4924
- const PLATFORM_EVENT_SOURCES_QUERY = gql`
4924
+ const PLATFORM_EVENT_SOURCES_QUERY = gql$1`
4925
4925
  query PlatformEventSources {
4926
4926
  platformEventSources {
4927
4927
  module
@@ -4935,17 +4935,17 @@ const PLATFORM_EVENT_SOURCES_QUERY = gql`
4935
4935
  /** The list hardly changes; one read per few minutes is plenty. */
4936
4936
  const CACHE_TTL_MS$1 = 18e4;
4937
4937
  const caches$1 = /* @__PURE__ */ new WeakMap();
4938
- function isRecord$3(value) {
4938
+ function isRecord$4(value) {
4939
4939
  return typeof value === "object" && value !== null && !Array.isArray(value);
4940
4940
  }
4941
4941
  /** The well-formed sources of a `platformEventSources` response; anything else is dropped. */
4942
4942
  function readPlatformEventSources(data) {
4943
- const list = isRecord$3(data) ? data.platformEventSources : void 0;
4943
+ const list = isRecord$4(data) ? data.platformEventSources : void 0;
4944
4944
  if (!Array.isArray(list)) return [];
4945
4945
  const sources = [];
4946
4946
  for (const item of list) {
4947
- if (!isRecord$3(item) || typeof item.module !== "string") continue;
4948
- const recordTypes = Array.isArray(item.recordTypes) ? item.recordTypes.flatMap((type) => isRecord$3(type) && typeof type.name === "string" ? [{
4947
+ if (!isRecord$4(item) || typeof item.module !== "string") continue;
4948
+ const recordTypes = Array.isArray(item.recordTypes) ? item.recordTypes.flatMap((type) => isRecord$4(type) && typeof type.name === "string" ? [{
4949
4949
  name: type.name,
4950
4950
  actions: Array.isArray(type.actions) ? type.actions.filter((action) => typeof action === "string") : []
4951
4951
  }] : []) : [];
@@ -5194,7 +5194,7 @@ function PlatformEventSourceSelect({ kind, value, module = "", required = false,
5194
5194
  // src/builder/components/ndv/timecampus-time-triggers.ts
5195
5195
  /** Builder field type the TimeCampus Event manifest gives its `triggerIds` field. */
5196
5196
  const TIMECAMPUS_TIME_TRIGGER_FIELD = "timecampus-time-trigger";
5197
- const MY_TIME_TRIGGERS_QUERY = gql`
5197
+ const MY_TIME_TRIGGERS_QUERY = gql$1`
5198
5198
  query FluidGridsTimeCampusMyTimeTriggers {
5199
5199
  timeCampusMyTimeTriggers {
5200
5200
  id
@@ -5212,16 +5212,16 @@ const MY_TIME_TRIGGERS_QUERY = gql`
5212
5212
  */
5213
5213
  const CACHE_TTL_MS = 3e4;
5214
5214
  const caches = /* @__PURE__ */ new WeakMap();
5215
- function isRecord$2(value) {
5215
+ function isRecord$3(value) {
5216
5216
  return typeof value === "object" && value !== null && !Array.isArray(value);
5217
5217
  }
5218
5218
  /** The well-formed triggers of a `timeCampusMyTimeTriggers` response, sorted by name. */
5219
5219
  function readTimeTriggers(data) {
5220
- const list = isRecord$2(data) ? data.timeCampusMyTimeTriggers : void 0;
5220
+ const list = isRecord$3(data) ? data.timeCampusMyTimeTriggers : void 0;
5221
5221
  if (!Array.isArray(list)) return [];
5222
5222
  const triggers = [];
5223
5223
  for (const item of list) {
5224
- if (!isRecord$2(item) || typeof item.id !== "string" || !item.id) continue;
5224
+ if (!isRecord$3(item) || typeof item.id !== "string" || !item.id) continue;
5225
5225
  triggers.push({
5226
5226
  id: item.id,
5227
5227
  name: typeof item.name === "string" && item.name.trim() ? item.name.trim() : item.id,
@@ -5561,7 +5561,7 @@ function useGlobalStoreClient() {
5561
5561
  * Channels represent configured connections to communication platforms
5562
5562
  * (Slack, Teams, Discord, Email, SMS, etc.).
5563
5563
  */
5564
- const CHANNEL_SUMMARY_FIELDS = gql`
5564
+ const CHANNEL_SUMMARY_FIELDS = gql$1`
5565
5565
  fragment ChannelSummaryFields on Channel {
5566
5566
  id
5567
5567
  name
@@ -5575,7 +5575,7 @@ const CHANNEL_SUMMARY_FIELDS = gql`
5575
5575
  createdAt
5576
5576
  }
5577
5577
  `;
5578
- const CHANNEL_DETAIL_FIELDS = gql`
5578
+ const CHANNEL_DETAIL_FIELDS = gql$1`
5579
5579
  fragment ChannelDetailFields on Channel {
5580
5580
  id
5581
5581
  name
@@ -5597,7 +5597,7 @@ const CHANNEL_DETAIL_FIELDS = gql`
5597
5597
  updatedAt
5598
5598
  }
5599
5599
  `;
5600
- const GET_CHANNELS = gql`
5600
+ const GET_CHANNELS = gql$1`
5601
5601
  ${CHANNEL_SUMMARY_FIELDS}
5602
5602
  query GetChannels(
5603
5603
  $filter: ChannelFilterInput
@@ -5612,7 +5612,7 @@ const GET_CHANNELS = gql`
5612
5612
  }
5613
5613
  }
5614
5614
  `;
5615
- const GET_CHANNEL = gql`
5615
+ const GET_CHANNEL = gql$1`
5616
5616
  ${CHANNEL_DETAIL_FIELDS}
5617
5617
  query GetChannel($id: ID!) {
5618
5618
  getChannel(id: $id) {
@@ -5861,7 +5861,7 @@ function channelAuthUrl(providerKey, returnTo, nodeId) {
5861
5861
  * panel: pick a communication channel, see its status, start the platform's
5862
5862
  * OAuth flow, and link an integration connection to the channel.
5863
5863
  */
5864
- const LINK_INTEGRATION_MUTATION = gql`
5864
+ const LINK_INTEGRATION_MUTATION = gql$1`
5865
5865
  mutation LinkIntegrationConnection(
5866
5866
  $integrationConnectionId: ID!
5867
5867
  $channelId: ID!
@@ -8054,7 +8054,7 @@ function NodeParamsForm({ config, nodeType, testIdPrefix = "ndv" }) {
8054
8054
  * nodes whose "Run on" names it execute there, in a local sandbox. Mirrors the
8055
8055
  * connections/variables hook pattern (shared Apollo client, no generated hooks).
8056
8056
  */
8057
- const RUNNER_FIELDS = gql`
8057
+ const RUNNER_FIELDS = gql$1`
8058
8058
  fragment SandboxRunnerFields on SandboxRunner {
8059
8059
  id
8060
8060
  name
@@ -8077,7 +8077,7 @@ const RUNNER_FIELDS = gql`
8077
8077
  createdAt
8078
8078
  }
8079
8079
  `;
8080
- const LIST_RUNNERS = gql`
8080
+ const LIST_RUNNERS = gql$1`
8081
8081
  ${RUNNER_FIELDS}
8082
8082
  query SandboxRunners {
8083
8083
  sandboxRunners(context: {}) {
@@ -8085,7 +8085,7 @@ const LIST_RUNNERS = gql`
8085
8085
  }
8086
8086
  }
8087
8087
  `;
8088
- const CREATE_RUNNER = gql`
8088
+ const CREATE_RUNNER = gql$1`
8089
8089
  ${RUNNER_FIELDS}
8090
8090
  mutation CreateSandboxRunner($name: String!) {
8091
8091
  createSandboxRunner(input: { name: $name, context: {} }) {
@@ -8097,7 +8097,7 @@ const CREATE_RUNNER = gql`
8097
8097
  }
8098
8098
  }
8099
8099
  `;
8100
- const ROTATE_RUNNER_TOKEN = gql`
8100
+ const ROTATE_RUNNER_TOKEN = gql$1`
8101
8101
  ${RUNNER_FIELDS}
8102
8102
  mutation RotateSandboxRunnerToken($id: ID!) {
8103
8103
  rotateSandboxRunnerToken(id: $id, context: {}) {
@@ -8109,7 +8109,7 @@ const ROTATE_RUNNER_TOKEN = gql`
8109
8109
  }
8110
8110
  }
8111
8111
  `;
8112
- const DELETE_RUNNER = gql`
8112
+ const DELETE_RUNNER = gql$1`
8113
8113
  mutation DeleteSandboxRunner($id: ID!) {
8114
8114
  deleteSandboxRunner(id: $id, context: {})
8115
8115
  }
@@ -9913,214 +9913,1032 @@ function useCreateOrder() {
9913
9913
  };
9914
9914
  }
9915
9915
 
9916
- // src/hooks/store/useOrganizationPrivateNodes.ts
9917
- function errorMessage(error, fallback) {
9918
- return error instanceof Error && error.message ? error.message : fallback;
9919
- }
9916
+ // src/hooks/store/useTemplateDetail.ts
9917
+ const GET_WORKFLOW_TEMPLATE_DETAIL = gql`
9918
+ query GetWorkflowTemplateDetail($id: ID!) {
9919
+ product(id: $id) {
9920
+ id
9921
+ name
9922
+ slug
9923
+ type
9924
+ description
9925
+ longDescription
9926
+ icon
9927
+ screenshots
9928
+ videoUrls
9929
+ rating
9930
+ reviewCount
9931
+ downloads
9932
+ viewCount
9933
+ price
9934
+ pricingModel
9935
+ currency
9936
+ featured
9937
+ content
9938
+ manifest
9939
+ manifestVersion
9940
+ authorName
9941
+ license
9942
+ category
9943
+ tags
9944
+ compatibleProducts
9945
+ publisher {
9946
+ id
9947
+ name
9948
+ email
9949
+ websiteUrl
9950
+ logoUrl
9951
+ isVerified
9952
+ }
9953
+ createdAt
9954
+ updatedAt
9955
+ publishedAt
9956
+ }
9957
+ }
9958
+ `;
9920
9959
  /**
9921
- * The workspace-private custom nodes of the current workspace's organization
9922
- * (BOFF-9766), with install, update and uninstall.
9960
+ * Hook to lazily fetch full details of a workflow template from the store.
9923
9961
  *
9924
- * These nodes skip the public store: devportal's `organizationPrivateNodes`
9925
- * lists them, `installApp` installs a chosen version (which also registers
9926
- * it in the workspace's builder; installing a newer version over an active
9927
- * install updates it), and `uninstallApp` removes it. After every change the
9928
- * builder's node palette is refreshed so the node appears or disappears.
9962
+ * Unlike `useWorkflowTemplates` which fetches browse-level metadata,
9963
+ * this hook fetches the full product detail including the `content` field
9964
+ * which contains the workflow definitionJson.
9965
+ *
9966
+ * Uses a manual query (not useQuery) to only fetch when explicitly requested.
9929
9967
  */
9930
- function useOrganizationPrivateNodes() {
9968
+ function useTemplateDetail() {
9931
9969
  const client = useGlobalStoreClient();
9932
- const workspaceId = useContext(FluidGridsContext)?.workspaceId ?? getFluidGridsConfig().workspaceId;
9933
- const { refreshAvailableNodes } = useRefreshAvailableNodes();
9934
- const [busyApplicationId, setBusyApplicationId] = useState(null);
9935
- const { data, loading, error, refetch } = useQuery(OrganizationPrivateNodesDocument, {
9936
- client,
9937
- variables: { workspaceId: workspaceId || "" },
9938
- skip: !workspaceId,
9939
- fetchPolicy: "cache-and-network"
9940
- });
9941
- const [installMutation] = useMutation(InstallOrganizationPrivateNodeDocument, { client });
9942
- const [uninstallMutation] = useMutation(UninstallOrganizationPrivateNodeDocument, { client });
9943
- const afterChange = useCallback(async () => {
9944
- await refreshAvailableNodes();
9945
- await refetch();
9946
- }, [refreshAvailableNodes, refetch]);
9947
- /** Install the node's latest organization version, or update to it. */
9948
- const install = useCallback(async (node) => {
9949
- if (!workspaceId) return {
9950
- success: false,
9951
- error: "No workspace"
9952
- };
9953
- setBusyApplicationId(node.applicationId);
9954
- try {
9955
- const result = await installMutation({ variables: {
9956
- applicationId: node.applicationId,
9957
- workspaceId,
9958
- version: node.version
9959
- } });
9960
- if (result.error || !result.data?.installApp) throw result.error ?? /* @__PURE__ */ new Error("Installation failed");
9961
- await afterChange();
9962
- return { success: true };
9963
- } catch (err) {
9964
- return {
9965
- success: false,
9966
- error: errorMessage(err, "Installation failed")
9967
- };
9968
- } finally {
9969
- setBusyApplicationId(null);
9970
- }
9971
- }, [
9972
- afterChange,
9973
- installMutation,
9974
- workspaceId
9975
- ]);
9976
- const uninstall = useCallback(async (node) => {
9977
- if (!node.installationId) return {
9978
- success: false,
9979
- error: "The node is not installed"
9980
- };
9981
- setBusyApplicationId(node.applicationId);
9982
- try {
9983
- const result = await uninstallMutation({ variables: { installationId: node.installationId } });
9984
- if (result.error || result.data?.uninstallApp !== true) throw result.error ?? /* @__PURE__ */ new Error("Uninstall failed");
9985
- await afterChange();
9986
- return { success: true };
9987
- } catch (err) {
9988
- return {
9989
- success: false,
9990
- error: errorMessage(err, "Uninstall failed")
9991
- };
9992
- } finally {
9993
- setBusyApplicationId(null);
9994
- }
9995
- }, [afterChange, uninstallMutation]);
9996
- const listing = data?.organizationPrivateNodes;
9970
+ const [loading, setLoading] = useState(false);
9971
+ const [error, setError] = useState(null);
9972
+ const context = useContext(FluidGridsContext);
9973
+ const workspaceId = context?.workspaceId ?? getFluidGridsConfig().workspaceId;
9974
+ const organizationId = context?.organizationId;
9975
+ const contextHeaders = useMemo(() => {
9976
+ const headers = {};
9977
+ if (organizationId) headers["x-organization-id"] = organizationId;
9978
+ if (workspaceId) headers["x-workspace-id"] = workspaceId;
9979
+ return headers;
9980
+ }, [organizationId, workspaceId]);
9997
9981
  return {
9998
- nodes: listing?.nodes ?? [],
9999
- canManage: listing?.canManage ?? false,
9982
+ fetchTemplateDetail: useCallback(async (templateId) => {
9983
+ setLoading(true);
9984
+ setError(null);
9985
+ try {
9986
+ const result = await client.query({
9987
+ query: GET_WORKFLOW_TEMPLATE_DETAIL,
9988
+ variables: { id: templateId },
9989
+ fetchPolicy: "network-only",
9990
+ context: Object.keys(contextHeaders).length > 0 ? { headers: contextHeaders } : void 0
9991
+ });
9992
+ if (result.error) throw result.error;
9993
+ return result.data?.product ?? null;
9994
+ } catch (err) {
9995
+ const fetchError = err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to fetch template detail");
9996
+ setError(fetchError);
9997
+ console.error("[useTemplateDetail] Error:", err);
9998
+ return null;
9999
+ } finally {
10000
+ setLoading(false);
10001
+ }
10002
+ }, [client, contextHeaders]),
10000
10003
  loading,
10001
- error,
10002
- busyApplicationId,
10003
- install,
10004
- uninstall,
10005
- refetch
10004
+ error
10006
10005
  };
10007
10006
  }
10008
10007
 
10009
- // src/builder/components/flow-builder/components/blocks/sidebar/panels/store-browse/components/store-node-list-item.tsx
10010
- function StoreNodeListItem({ name, description, icon, isInstalled, isPrebuilt, hasUpdate, isDeprecated, nodeType, isDraggable, onClick }) {
10011
- const tr = useTr();
10012
- const displayIcon = getStoreNodeIcon(icon, nodeType);
10013
- const handleDragStart = useCallback((e) => {
10014
- if (!isDraggable || !nodeType) return;
10015
- e.dataTransfer.setData(NODE_TYPE_DRAG_DATA_FORMAT, nodeType);
10016
- e.dataTransfer.effectAllowed = "move";
10017
- }, [isDraggable, nodeType]);
10018
- return /* @__PURE__ */ jsxs("button", {
10019
- onClick,
10020
- draggable: isDraggable && !!nodeType,
10021
- onDragStart: handleDragStart,
10022
- title: isDraggable ? `${name} — drag to add to canvas` : name,
10023
- className: "w-full flex items-center gap-3 px-2 py-2.5 rounded-md hover:bg-muted/60 transition-colors text-left group",
10024
- style: isDraggable ? { cursor: "grab" } : void 0,
10025
- children: [
10026
- /* @__PURE__ */ jsx("div", {
10027
- className: "relative size-9 shrink-0 rounded-lg bg-muted flex items-center justify-center overflow-hidden",
10028
- children: displayIcon ? isImageIcon(displayIcon) ? /* @__PURE__ */ jsx("img", {
10029
- src: displayIcon,
10030
- alt: name,
10031
- className: "w-full h-full object-cover"
10032
- }) : /* @__PURE__ */ jsx(Icon, {
10033
- icon: displayIcon,
10034
- className: "size-5 text-muted-foreground"
10035
- }) : /* @__PURE__ */ jsx("span", {
10036
- className: "text-sm font-bold text-muted-foreground",
10037
- children: name.charAt(0).toUpperCase()
10038
- })
10039
- }),
10040
- /* @__PURE__ */ jsxs("div", {
10041
- className: "flex-1 min-w-0",
10042
- children: [/* @__PURE__ */ jsxs("div", {
10043
- className: "flex items-center gap-1.5 flex-wrap",
10044
- children: [
10045
- /* @__PURE__ */ jsx("span", {
10046
- className: "text-sm font-medium text-foreground group-hover:text-primary transition-colors truncate",
10047
- children: name
10048
- }),
10049
- isDeprecated && /* @__PURE__ */ jsx("span", {
10050
- title: tr("fluidgrids.builder.storeNodeListItem.deprecatedTooltip", "This node is no longer supported. Consider migrating to a newer alternative."),
10051
- className: "shrink-0 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-semibold cursor-help",
10052
- style: {
10053
- background: "var(--color-status-warning-bgSubtle)",
10054
- color: "var(--color-status-warning-text)"
10055
- },
10056
- children: tr("fluidgrids.builder.storeNodeListItem.deprecatedBadge", "Deprecated")
10057
- }),
10058
- hasUpdate && !isDeprecated && /* @__PURE__ */ jsx("span", {
10059
- className: "shrink-0 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold",
10060
- style: {
10061
- background: "var(--color-status-warning-bgSubtle)",
10062
- color: "var(--color-status-warning-text)"
10063
- },
10064
- children: tr("fluidgrids.builder.storeNodeListItem.updateBadge", "Update")
10065
- }),
10066
- isInstalled && !hasUpdate && !isDeprecated && /* @__PURE__ */ jsxs("span", {
10067
- className: "shrink-0 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-semibold",
10068
- style: {
10069
- background: "var(--color-status-success-bgSubtle)",
10070
- color: "var(--color-status-success-text)"
10071
- },
10072
- children: [/* @__PURE__ */ jsx("span", {
10073
- className: "size-1.5 rounded-full inline-block",
10074
- style: { background: "var(--color-status-success-bg)" }
10075
- }), tr("fluidgrids.builder.storeNodeListItem.installedBadge", "Installed")]
10076
- }),
10077
- isPrebuilt && !isInstalled && !hasUpdate && !isDeprecated && /* @__PURE__ */ jsxs("span", {
10078
- className: "shrink-0 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-semibold",
10079
- style: {
10080
- background: "var(--color-status-info-bgSubtle)",
10081
- color: "var(--color-status-info-text)"
10082
- },
10083
- children: [/* @__PURE__ */ jsx("span", {
10084
- className: "size-1.5 rounded-full inline-block",
10085
- style: { background: "var(--color-status-info-bg)" }
10086
- }), tr("fluidgrids.builder.storeNodeListItem.prebuiltBadge", "Prebuilt")]
10087
- })
10088
- ]
10089
- }), description && /* @__PURE__ */ jsx("p", {
10090
- className: "text-xs text-muted-foreground truncate mt-0.5 leading-snug",
10091
- children: description
10092
- })]
10093
- }),
10094
- isDraggable ? /* @__PURE__ */ jsx(Icon, {
10095
- icon: "mynaui:drag-dots-vertical",
10096
- className: "size-4 shrink-0 text-foreground/40 group-hover:text-primary transition-colors"
10097
- }) : /* @__PURE__ */ jsx(Icon, {
10098
- icon: "mynaui:chevron-right",
10099
- className: "size-4 shrink-0 text-foreground/40"
10100
- })
10101
- ]
10008
+ // src/utils/template-input-applicator.ts
10009
+ /**
10010
+ * Template Input Applicator
10011
+ *
10012
+ * Walks a workflow definitionJson and replaces ${variableName} placeholders
10013
+ * with user-provided values. Used during template import to apply
10014
+ * user-configurable inputs before creating the workflow.
10015
+ */
10016
+ /**
10017
+ * Deep-clone an object using structured clone.
10018
+ * Falls back to JSON parse/stringify for environments without structuredClone.
10019
+ */
10020
+ function deepClone(obj) {
10021
+ if (typeof structuredClone === "function") return structuredClone(obj);
10022
+ return JSON.parse(JSON.stringify(obj));
10023
+ }
10024
+ /**
10025
+ * Apply template input values to a definitionJson by replacing
10026
+ * ${variableName} placeholders in string values throughout the object tree.
10027
+ *
10028
+ * @param definitionJson - The workflow definition JSON to process
10029
+ * @param inputs - Map of variable names to user-provided values
10030
+ * @returns A new definitionJson with placeholders replaced
10031
+ *
10032
+ * @example
10033
+ * ```ts
10034
+ * const definition = {
10035
+ * nodes: [{
10036
+ * data: { url: "${slack_webhook_url}", message: "Hello ${name}" }
10037
+ * }]
10038
+ * };
10039
+ * const result = applyTemplateInputs(definition, {
10040
+ * slack_webhook_url: "https://hooks.slack.com/...",
10041
+ * name: "World"
10042
+ * });
10043
+ * // result.nodes[0].data.url === "https://hooks.slack.com/..."
10044
+ * // result.nodes[0].data.message === "Hello World"
10045
+ * ```
10046
+ */
10047
+ function applyTemplateInputs(definitionJson, inputs) {
10048
+ if (!inputs || Object.keys(inputs).length === 0) return definitionJson;
10049
+ return walkAndReplace(deepClone(definitionJson), inputs);
10050
+ }
10051
+ /**
10052
+ * Recursively walk an object/array and replace ${variableName} placeholders
10053
+ * in string values.
10054
+ */
10055
+ function walkAndReplace(obj, inputs) {
10056
+ if (typeof obj === "string") return replaceVariables(obj, inputs);
10057
+ if (Array.isArray(obj)) return obj.map((item) => walkAndReplace(item, inputs));
10058
+ if (obj !== null && typeof obj === "object") {
10059
+ const result = {};
10060
+ for (const [key, value] of Object.entries(obj)) result[key] = walkAndReplace(value, inputs);
10061
+ return result;
10062
+ }
10063
+ return obj;
10064
+ }
10065
+ /**
10066
+ * Replace ${variableName} patterns in a string with provided values.
10067
+ *
10068
+ * Supports:
10069
+ * - ${variableName} — standard replacement
10070
+ * - ${variableName || 'default'} — fallback to default if variable not provided
10071
+ */
10072
+ function replaceVariables(str, inputs) {
10073
+ return str.replace(/\$\{([^}]+)\}/g, (_match, expression) => {
10074
+ const trimmed = expression.trim();
10075
+ const fallbackMatch = trimmed.match(/^(\w+)\s*\|\|\s*['"]?(.+?)['"]?$/);
10076
+ if (fallbackMatch) {
10077
+ const varName = fallbackMatch[1];
10078
+ const fallback = fallbackMatch[2];
10079
+ if (varName in inputs) return String(inputs[varName]);
10080
+ return fallback;
10081
+ }
10082
+ if (trimmed in inputs) return String(inputs[trimmed]);
10083
+ return _match;
10102
10084
  });
10103
10085
  }
10104
10086
 
10105
- // src/builder/components/flow-builder/components/blocks/sidebar/panels/store-browse/components/organization-private-nodes.tsx
10106
- const KEY = "fluidgrids.builder.organizationPrivateNodes";
10107
- function NodeIcon({ name, icon }) {
10108
- return /* @__PURE__ */ jsx("div", {
10109
- className: "size-9 shrink-0 rounded-lg bg-muted flex items-center justify-center overflow-hidden",
10110
- children: icon ? isImageIcon(icon) ? /* @__PURE__ */ jsx("img", {
10111
- src: icon,
10112
- alt: name,
10113
- className: "w-full h-full object-cover"
10114
- }) : /* @__PURE__ */ jsx(Icon, {
10115
- icon,
10116
- className: "size-5 text-muted-foreground"
10117
- }) : /* @__PURE__ */ jsx("span", {
10118
- className: "text-sm font-bold text-muted-foreground",
10119
- children: name.charAt(0).toUpperCase()
10120
- })
10121
- });
10087
+ // src/utils/template-import-flow.ts
10088
+ function isRecord$2(value) {
10089
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10122
10090
  }
10123
- function OrganizationPrivateNodeRow({ node, canManage, busy, disabled, onInstall, onUninstall }) {
10091
+ function graphOf(value) {
10092
+ if (!Array.isArray(value.nodes)) return null;
10093
+ return {
10094
+ nodes: value.nodes,
10095
+ edges: Array.isArray(value.edges) ? value.edges : [],
10096
+ name: typeof value.name === "string" ? value.name : void 0
10097
+ };
10098
+ }
10099
+ /**
10100
+ * The canvas of a WORKFLOW product that keeps it in its manifest instead of
10101
+ * `content.definitionJson`: either a flat `{ nodes, edges }` manifest, or the
10102
+ * Store Listing JSON editor's `manifest.jsonContent` string. Null when the
10103
+ * manifest holds neither.
10104
+ */
10105
+ function manifestTemplateGraph(manifest) {
10106
+ if (!isRecord$2(manifest)) return null;
10107
+ const direct = graphOf(manifest);
10108
+ if (direct) return direct;
10109
+ if (typeof manifest.jsonContent !== "string") return null;
10110
+ try {
10111
+ const parsed = JSON.parse(manifest.jsonContent);
10112
+ return isRecord$2(parsed) ? graphOf(parsed) : null;
10113
+ } catch {
10114
+ return null;
10115
+ }
10116
+ }
10117
+ /**
10118
+ * The template content a product carries: `content` when it holds a
10119
+ * `definitionJson`, otherwise a content built from the manifest's canvas,
10120
+ * with its required nodes worked out by `requiredNodeTypesOf` (a failed
10121
+ * lookup leaves them empty, which skips the node check). Null when the
10122
+ * product has no workflow at all.
10123
+ */
10124
+ async function resolveTemplateContent(product, requiredNodeTypesOf) {
10125
+ const content = product.content;
10126
+ if (content?.definitionJson) return content;
10127
+ const graph = manifestTemplateGraph(product.manifest);
10128
+ if (!graph) return null;
10129
+ let requiredNodeTypes = [];
10130
+ try {
10131
+ requiredNodeTypes = await requiredNodeTypesOf(graph.nodes);
10132
+ } catch {
10133
+ requiredNodeTypes = [];
10134
+ }
10135
+ return {
10136
+ definitionJson: {
10137
+ nodes: graph.nodes,
10138
+ edges: graph.edges
10139
+ },
10140
+ requiredNodeTypes,
10141
+ defaultWorkflowName: graph.name ?? product.name
10142
+ };
10143
+ }
10144
+ /**
10145
+ * The canvas to create a copy from. Some templates store a whole API
10146
+ * response as their definition (`{ data: { workflowDefinition: {
10147
+ * definitionJson } } }`); the canvas is the inner `definitionJson`.
10148
+ */
10149
+ function unwrapTemplateDefinition(definitionJson) {
10150
+ const data = definitionJson.data;
10151
+ if (!isRecord$2(data)) return definitionJson;
10152
+ const nested = isRecord$2(data.workflowDefinition) ? data.workflowDefinition.definitionJson : void 0;
10153
+ return isRecord$2(nested) ? nested : definitionJson;
10154
+ }
10155
+ /**
10156
+ * Whether "Use this template" must stop at the configuration dialog before
10157
+ * creating the copy: the template asks for inputs or connections, or one of
10158
+ * its nodes is not installed (free to install, paid, or not offered by the
10159
+ * store at all).
10160
+ */
10161
+ function needsTemplateConfiguration(content, nodeCheck) {
10162
+ return (content.requiredInputs?.length ?? 0) > 0 || (content.requiredCredentialTypes?.length ?? 0) > 0 || nodeCheck.missing.length > 0 || nodeCheck.missingPaid.length > 0 || (nodeCheck.unavailable?.length ?? 0) > 0;
10163
+ }
10164
+ /** The free nodes to install before the copy is created. */
10165
+ function nodesToInstall(nodeCheck) {
10166
+ return (nodeCheck?.missing ?? []).filter((node) => !!node.storeProductId);
10167
+ }
10168
+ /**
10169
+ * Install nodes one at a time, in order, reporting progress before each.
10170
+ * One at a time on purpose: the dialog shows "installing i of n", and every
10171
+ * install writes the same workspace's installation records in the store.
10172
+ * A failed install is reported to `onFailure` and the rest still run, so the
10173
+ * copy can be created; the user installs the node later from the Node Store.
10174
+ * Resolves to the node types that installed.
10175
+ */
10176
+ async function installNodesInOrder(nodes, install, onProgress, onFailure) {
10177
+ const installed = [];
10178
+ await nodes.reduce((previous, node, index) => previous.then(async () => {
10179
+ onProgress({
10180
+ current: index + 1,
10181
+ total: nodes.length,
10182
+ nodeName: node.displayName || node.nodeType
10183
+ });
10184
+ try {
10185
+ await install(node);
10186
+ installed.push(node.nodeType);
10187
+ } catch (error) {
10188
+ onFailure(node, error);
10189
+ }
10190
+ }), Promise.resolve());
10191
+ return installed;
10192
+ }
10193
+
10194
+ // src/hooks/store/useImportWorkflowTemplate.ts
10195
+ const IMPORT_WORKFLOW_TEMPLATE = gql`
10196
+ mutation ImportWorkflowTemplate($input: ImportWorkflowTemplateInput!) {
10197
+ importWorkflowTemplate(input: $input) {
10198
+ success
10199
+ message
10200
+ workflow {
10201
+ id
10202
+ workflowKey
10203
+ version
10204
+ name
10205
+ description
10206
+ status
10207
+ definitionJson
10208
+ tags
10209
+ category
10210
+ icon
10211
+ createdAt
10212
+ }
10213
+ }
10214
+ }
10215
+ `;
10216
+ const RECORD_DOWNLOAD = gql`
10217
+ mutation RecordDownload($productId: String!) {
10218
+ recordDownload(productId: $productId) {
10219
+ id
10220
+ downloadedAt
10221
+ }
10222
+ }
10223
+ `;
10224
+ const WORKFLOW_DEFINITIONS = gql`
10225
+ query WorkflowDefinitions($input: ListWorkflowDefinitionsInput!) {
10226
+ workflowDefinitions(input: $input) {
10227
+ items {
10228
+ id
10229
+ workflowKey
10230
+ name
10231
+ version
10232
+ status
10233
+ }
10234
+ total
10235
+ hasMore
10236
+ }
10237
+ }
10238
+ `;
10239
+ /**
10240
+ * Hook to orchestrate importing a workflow template from the store.
10241
+ *
10242
+ * It fetches template detail, parses content and creates the copy:
10243
+ * 1. Fetch full template detail from global store (lazy load content)
10244
+ * 2. Apply user-provided template inputs to definitionJson
10245
+ * 3. Create workflow via importWorkflowTemplate mutation (workspace gateway)
10246
+ * 4. Record the download and the installation in the store
10247
+ *
10248
+ * The node check, the configuration dialog and installing missing nodes are
10249
+ * done by `useTemplateImportFlow`, which every "Use this template" button
10250
+ * goes through (BOFF-11153).
10251
+ */
10252
+ function useImportWorkflowTemplate() {
10253
+ const [importing, setImporting] = useState(false);
10254
+ const [error, setError] = useState(null);
10255
+ const { fetchTemplateDetail, loading: fetchingDetail } = useTemplateDetail();
10256
+ const globalClient = useGlobalStoreClient();
10257
+ const context = useContext(FluidGridsContext);
10258
+ const workspaceId = context?.workspaceId ?? getFluidGridsConfig().workspaceId;
10259
+ const bus = useEventBus();
10260
+ const getTemplateDetail = useCallback(async (templateId) => {
10261
+ return fetchTemplateDetail(templateId);
10262
+ }, [fetchTemplateDetail]);
10263
+ /**
10264
+ * Parse the template content from a StoreProduct.
10265
+ * Returns null if the product doesn't have valid workflow template content.
10266
+ */
10267
+ const parseTemplateContent = useCallback((template) => {
10268
+ try {
10269
+ const content = template.content;
10270
+ if (!content?.definitionJson) {
10271
+ console.warn("[useImportWorkflowTemplate] Template has no definitionJson in content:", template.id);
10272
+ return null;
10273
+ }
10274
+ return content;
10275
+ } catch {
10276
+ console.error("[useImportWorkflowTemplate] Failed to parse template content:", template.id);
10277
+ return null;
10278
+ }
10279
+ }, []);
10280
+ return {
10281
+ /** Fetch full template content from store (lazy load) */
10282
+ getTemplateDetail,
10283
+ /** Parse WorkflowTemplateContent from a StoreProduct */
10284
+ parseTemplateContent,
10285
+ /** Full import: apply inputs -> create workflow -> record analytics */
10286
+ importTemplate: useCallback(async (template, options) => {
10287
+ setImporting(true);
10288
+ setError(null);
10289
+ try {
10290
+ const content = options?.content ?? parseTemplateContent(template);
10291
+ if (!content) throw new Error("Template does not contain valid workflow definition data");
10292
+ let definitionJson = unwrapTemplateDefinition(deepClone(content.definitionJson));
10293
+ if (options?.templateInputs) definitionJson = applyTemplateInputs(definitionJson, options.templateInputs);
10294
+ const config = getFluidGridsConfig();
10295
+ const importContext = {
10296
+ ...options?.context || {},
10297
+ ...config.productId ? { productId: config.productId } : {},
10298
+ ...config.productName ? { productName: config.productName } : {}
10299
+ };
10300
+ const client = getFluidGridsClient();
10301
+ const importResult = throwIfResultError(await client.mutate({
10302
+ mutation: IMPORT_WORKFLOW_TEMPLATE,
10303
+ variables: { input: {
10304
+ definitionJson,
10305
+ name: options?.name || content.defaultWorkflowName || template.name,
10306
+ description: options?.description || template.description,
10307
+ sourceTemplateId: template.id,
10308
+ sourceTemplateName: template.name,
10309
+ tags: content.defaultTags || template.tags || [],
10310
+ category: content.defaultCategory || template.category || void 0,
10311
+ icon: template.icon || void 0,
10312
+ projectId: options?.projectId,
10313
+ context: importContext
10314
+ } },
10315
+ refetchQueries: [WORKFLOW_DEFINITIONS]
10316
+ })).data?.importWorkflowTemplate;
10317
+ if (!importResult?.success || !importResult.workflow) throw new Error(importResult?.message || "Failed to create workflow from template");
10318
+ try {
10319
+ throwIfResultError(await globalClient.mutate({
10320
+ mutation: RECORD_DOWNLOAD,
10321
+ variables: { productId: template.id }
10322
+ }));
10323
+ } catch {
10324
+ console.warn("[useImportWorkflowTemplate] Failed to record store download");
10325
+ }
10326
+ try {
10327
+ throwIfResultError(await globalClient.mutate({
10328
+ mutation: InstallWorkflowNodeDocument,
10329
+ variables: { input: {
10330
+ productId: template.id,
10331
+ workspaceId,
10332
+ organizationId: context?.tenantId ?? getFluidGridsConfig().tenantId,
10333
+ version: template.manifestVersion || "1.0.0",
10334
+ manifest: template.manifest,
10335
+ metadata: {
10336
+ type: "WORKFLOW",
10337
+ sourceTemplateId: template.id,
10338
+ sourceTemplateName: template.name,
10339
+ importedWorkflowKey: importResult.workflow.workflowKey,
10340
+ importedWorkflowVersion: importResult.workflow.version
10341
+ }
10342
+ } }
10343
+ }));
10344
+ } catch {
10345
+ console.warn("[useImportWorkflowTemplate] Failed to record app installation");
10346
+ }
10347
+ emitWorkflowCreated(bus, {
10348
+ workspaceId,
10349
+ workflowId: importResult.workflow.id,
10350
+ source: "template",
10351
+ definition: definitionJson
10352
+ });
10353
+ emitTemplateUsed(bus, {
10354
+ workspaceId,
10355
+ templateId: template.id,
10356
+ category: content.defaultCategory || template.category
10357
+ });
10358
+ return {
10359
+ success: true,
10360
+ workflow: importResult.workflow,
10361
+ message: importResult.message || "Template imported successfully",
10362
+ installedNodes: [],
10363
+ requiredCredentials: content.requiredCredentialTypes || []
10364
+ };
10365
+ } catch (err) {
10366
+ const importError = err instanceof Error ? err : /* @__PURE__ */ new Error("Template import failed");
10367
+ setError(importError);
10368
+ console.error("[useImportWorkflowTemplate] Import failed:", err);
10369
+ return {
10370
+ success: false,
10371
+ workflow: null,
10372
+ message: importError.message,
10373
+ installedNodes: [],
10374
+ requiredCredentials: [],
10375
+ error: importError
10376
+ };
10377
+ } finally {
10378
+ setImporting(false);
10379
+ }
10380
+ }, [
10381
+ parseTemplateContent,
10382
+ globalClient,
10383
+ workspaceId,
10384
+ context?.tenantId,
10385
+ bus
10386
+ ]),
10387
+ /** Whether we're fetching template detail */
10388
+ fetchingDetail,
10389
+ /** Whether we're importing */
10390
+ importing,
10391
+ /** Last error */
10392
+ error
10393
+ };
10394
+ }
10395
+
10396
+ // src/utils/workflow-node-check.ts
10397
+ /**
10398
+ * Canvas-only node types: they live on the React Flow canvas but are never
10399
+ * executed, never registered in NODE_CONFIGS and never published to the store.
10400
+ *
10401
+ * They must be excluded from the dependency check. A sticky note reaching it
10402
+ * was reported as `sticky-note v1.0 — Not installed` in the Missing Nodes
10403
+ * dialog on every open of an affected workflow, and because no store product
10404
+ * backs it the Install action could only ever be a no-op (BOFF-7267).
10405
+ *
10406
+ * Kept as a literal set rather than importing STICKY_NOTE_TYPE from the
10407
+ * builder so this module stays free of builder-component imports.
10408
+ */
10409
+ const CANVAS_ONLY_NODE_TYPES = /* @__PURE__ */ new Set(["sticky-note"]);
10410
+ /**
10411
+ * Resolve the set of unique executable node types used by a workflow.
10412
+ *
10413
+ * Canvas-only annotations (see CANVAS_ONLY_NODE_TYPES) are skipped: they carry
10414
+ * no dependency, so including them produces a permanently unsatisfiable
10415
+ * "missing node".
10416
+ */
10417
+ function getWorkflowNodeTypes(nodes) {
10418
+ return [...new Set(nodes.map((n) => n.type).filter((t) => Boolean(t)).filter((t) => !CANVAS_ONLY_NODE_TYPES.has(t)))];
10419
+ }
10420
+ /**
10421
+ * Page size for walking the node catalog. global-store-svc clamps
10422
+ * `browseStore` pages to 100 rows (MAX_PAGINATION_LIMIT), so asking for more
10423
+ * returns 100 anyway. The old single request for 200 rows silently dropped
10424
+ * every node past the first 100 (BOFF-11402).
10425
+ */
10426
+ const STORE_NODE_PAGE_SIZE = 100;
10427
+ /**
10428
+ * Safety net for the paging loop: 50 pages of 100 is far beyond the catalog
10429
+ * (about 175 node products in 2026-11). The loop normally ends on `hasMore`.
10430
+ */
10431
+ const STORE_NODE_MAX_PAGES = 50;
10432
+ /** The browseStore variables for one page of FluidGrids node products. */
10433
+ function storeNodePageVariables(offset) {
10434
+ return { input: {
10435
+ filter: {
10436
+ type: "NODE",
10437
+ compatibleWith: "fluidgrids"
10438
+ },
10439
+ sortBy: "DOWNLOADS",
10440
+ pagination: {
10441
+ limit: 100,
10442
+ offset
10443
+ }
10444
+ } };
10445
+ }
10446
+ /**
10447
+ * Fetch every FluidGrids-compatible node product from the store.
10448
+ *
10449
+ * `browseStore` pages by `offset`; its `cursor` input is ignored by the
10450
+ * server. The offset advances by the number of rows actually returned so a
10451
+ * lower server cap still walks the whole catalog. Rows are de-duplicated by
10452
+ * product id in case a page boundary moves while the catalog changes.
10453
+ */
10454
+ async function fetchAllStoreNodes(fetchPage) {
10455
+ const byId = /* @__PURE__ */ new Map();
10456
+ let offset = 0;
10457
+ for (let page = 0; page < 50; page++) {
10458
+ const browse = await fetchPage(storeNodePageVariables(offset));
10459
+ const products = browse?.products ?? [];
10460
+ for (const product of products) if (!byId.has(product.id)) byId.set(product.id, product);
10461
+ if (!browse?.hasMore || products.length === 0) break;
10462
+ offset += products.length;
10463
+ }
10464
+ return [...byId.values()];
10465
+ }
10466
+ function publishedAtMs(product) {
10467
+ const ms = product.publishedAt ? Date.parse(product.publishedAt) : NaN;
10468
+ return Number.isNaN(ms) ? Number.NEGATIVE_INFINITY : ms;
10469
+ }
10470
+ /**
10471
+ * Order two store products that offer the same nodeType; a negative result
10472
+ * means `a` is the better match.
10473
+ *
10474
+ * 1. A product linked to a devportal application (`productId` set) wins. Only
10475
+ * those have an installable published version; the unlinked rows are
10476
+ * orphaned seed rows such as `app-gmail-reader-001` that cannot install.
10477
+ * 2. Then more downloads, 3. then the most recently published, 4. then the
10478
+ * product id, so the choice never depends on the order the store returned.
10479
+ */
10480
+ function compareStoreNodeCandidates(a, b) {
10481
+ const aLinked = Boolean(a.productId);
10482
+ if (aLinked !== Boolean(b.productId)) return aLinked ? -1 : 1;
10483
+ const downloads = (b.downloads ?? 0) - (a.downloads ?? 0);
10484
+ if (downloads !== 0) return downloads;
10485
+ const aPublished = publishedAtMs(a);
10486
+ const bPublished = publishedAtMs(b);
10487
+ if (aPublished !== bPublished) return aPublished > bPublished ? -1 : 1;
10488
+ if (a.id === b.id) return 0;
10489
+ return a.id < b.id ? -1 : 1;
10490
+ }
10491
+ /**
10492
+ * Map nodeType -> best store product (see compareStoreNodeCandidates).
10493
+ * Pure function, independent of the order of `products`.
10494
+ */
10495
+ function buildStoreNodeTypeMap(products) {
10496
+ const nodeTypeToProduct = /* @__PURE__ */ new Map();
10497
+ for (const product of products) {
10498
+ const nodeType = product.manifest?.nodeType;
10499
+ if (typeof nodeType !== "string" || !nodeType) continue;
10500
+ const current = nodeTypeToProduct.get(nodeType);
10501
+ if (!current || compareStoreNodeCandidates(product, current) < 0) nodeTypeToProduct.set(nodeType, product);
10502
+ }
10503
+ return nodeTypeToProduct;
10504
+ }
10505
+ /**
10506
+ * Query the global store for every FluidGrids-compatible node product and map
10507
+ * nodeType string -> StoreProduct using manifest.nodeType.
10508
+ */
10509
+ async function fetchStoreNodeTypeMap() {
10510
+ const storeClient = getGlobalStoreClient();
10511
+ if (!storeClient) return /* @__PURE__ */ new Map();
10512
+ return buildStoreNodeTypeMap(await fetchAllStoreNodes(async (variables) => {
10513
+ return (await storeClient.query({
10514
+ query: BrowseWorkflowNodesDocument,
10515
+ variables,
10516
+ fetchPolicy: "cache-first"
10517
+ })).data?.browseStore;
10518
+ }));
10519
+ }
10520
+ /**
10521
+ * One required-node entry of a template (BOFF-11496): a type the registry
10522
+ * already has is BUILTIN at its registry version; any other type is a STORE
10523
+ * node at the node's own version from its store product. Versions are
10524
+ * canonical, never the product's manifest format version ("1.0").
10525
+ */
10526
+ function requiredNodeEntry(type, registered, product) {
10527
+ if (registered) return {
10528
+ nodeType: type,
10529
+ nodeVersion: canonicalNodeVersion(registered.version || "1.0.0"),
10530
+ publishType: "BUILTIN",
10531
+ displayName: type
10532
+ };
10533
+ return {
10534
+ nodeType: type,
10535
+ nodeVersion: storeNodeInstallVersion(product?.manifest, product?.manifestVersion),
10536
+ publishType: "STORE",
10537
+ storeProductId: product?.id,
10538
+ displayName: product?.name ?? type,
10539
+ requiresPurchase: product !== void 0 && product.pricingModel !== "FREE"
10540
+ };
10541
+ }
10542
+ /**
10543
+ * Build the list of required node types for a workflow.
10544
+ *
10545
+ * - BUILTIN / already-installed types are marked BUILTIN.
10546
+ * - Missing types are looked up in the store; if found, they are marked STORE
10547
+ * with storeProductId and requiresPurchase.
10548
+ * - Missing types not found in the store are still reported as STORE without
10549
+ * a storeProductId so the UI can show "not installed".
10550
+ */
10551
+ async function buildRequiredNodeTypes(nodes) {
10552
+ const workflowNodeTypes = getWorkflowNodeTypes(nodes);
10553
+ if (workflowNodeTypes.length === 0) return [];
10554
+ await ensureNodeTypesLoaded(workflowNodeTypes);
10555
+ const missingTypes = workflowNodeTypes.filter((type) => !NODE_CONFIGS.some((c) => c.type === type));
10556
+ let nodeTypeToProduct = /* @__PURE__ */ new Map();
10557
+ if (missingTypes.length > 0) nodeTypeToProduct = await fetchStoreNodeTypeMap();
10558
+ return workflowNodeTypes.map((type) => {
10559
+ const registered = NODE_CONFIGS.find((c) => c.type === type);
10560
+ return requiredNodeEntry(type, registered ? { version: registered.defaultData.version } : void 0, nodeTypeToProduct.get(type));
10561
+ });
10562
+ }
10563
+ /**
10564
+ * Classify required nodes against the set of installed store app IDs.
10565
+ *
10566
+ * Pure function — easy to unit test.
10567
+ */
10568
+ function buildNodeCheck(requiredNodeTypes, installedAppIds) {
10569
+ const installed = requiredNodeTypes.filter((n) => n.publishType === "BUILTIN" || n.storeProductId && installedAppIds.has(n.storeProductId));
10570
+ const notInstalled = requiredNodeTypes.filter((n) => n.publishType !== "BUILTIN" && (!n.storeProductId || !installedAppIds.has(n.storeProductId)));
10571
+ const unavailable = notInstalled.filter((n) => !n.storeProductId);
10572
+ const installable = notInstalled.filter((n) => !!n.storeProductId);
10573
+ const missing = installable.filter((n) => !n.requiresPurchase);
10574
+ const missingPaid = installable.filter((n) => !!n.requiresPurchase);
10575
+ return {
10576
+ installed,
10577
+ missing,
10578
+ missingPaid,
10579
+ unavailable,
10580
+ canImport: missingPaid.length === 0
10581
+ };
10582
+ }
10583
+
10584
+ // src/hooks/store/useTemplateImportFlow.tsx
10585
+ const LazyTemplateConfigurationModal = lazy(() => import("./TemplateConfigurationModal-ChQsXf0-.js").then((m) => ({ default: m.TemplateConfigurationModal })));
10586
+ /**
10587
+ * "Use this template", the same everywhere it is offered: the builder's
10588
+ * template panel, the Templates page and the template preview page
10589
+ * (BOFF-11153). Before this, only the builder panel checked the template's
10590
+ * nodes and asked for its inputs; the two pages created the copy directly.
10591
+ *
10592
+ * 1. Load the template's full detail and resolve its content.
10593
+ * 2. Check its required nodes against the workspace's installed nodes.
10594
+ * 3. When it asks for inputs or connections, or a node is not installed,
10595
+ * show the configuration dialog; otherwise go straight on.
10596
+ * 4. Install the missing free nodes, create the copy with the user's inputs,
10597
+ * and open it (`onImported`).
10598
+ */
10599
+ function useTemplateImportFlow({ onImported, onQuotaExhausted }) {
10600
+ const tr = useTr();
10601
+ const { bus, workspaceId } = useFluidGridsAnalytics();
10602
+ const { installedAppIds } = useInstalledNodes();
10603
+ const { getTemplateDetail, importTemplate } = useImportWorkflowTemplate();
10604
+ const [pending, setPending] = useState(null);
10605
+ const [busy, setBusy] = useState(false);
10606
+ const [installProgress, setInstallProgress] = useState(null);
10607
+ const installMissingNodes = useCallback(async (nodeCheck) => {
10608
+ const nodes = nodesToInstall(nodeCheck);
10609
+ const storeClient = getGlobalStoreClient();
10610
+ if (nodes.length === 0 || !storeClient) return;
10611
+ const config = getFluidGridsConfig();
10612
+ await installNodesInOrder(nodes, async (node) => {
10613
+ throwIfResultError(await storeClient.mutate({
10614
+ mutation: InstallWorkflowNodeDocument,
10615
+ variables: { input: {
10616
+ productId: node.storeProductId ?? "",
10617
+ workspaceId: config.workspaceId,
10618
+ organizationId: config.tenantId,
10619
+ version: node.nodeVersion,
10620
+ manifest: null
10621
+ } }
10622
+ }));
10623
+ emitNodeInstalled(bus, {
10624
+ workspaceId,
10625
+ nodeType: node.nodeType,
10626
+ source: "store"
10627
+ });
10628
+ }, setInstallProgress, (node, err) => console.warn(`[useTemplateImportFlow] Failed to install node ${node.nodeType}:`, err));
10629
+ setInstallProgress(null);
10630
+ }, [bus, workspaceId]);
10631
+ const createCopy = useCallback(async (template, content, nodeCheck, overrides) => {
10632
+ setBusy(true);
10633
+ try {
10634
+ await installMissingNodes(nodeCheck);
10635
+ toast.loading(tr("fluidgrids.builder.desktopSidebar.creatingWorkflowToast", "Creating workflow..."), { id: "template-import" });
10636
+ const result = await importTemplate(template, {
10637
+ content,
10638
+ name: overrides?.name,
10639
+ templateInputs: overrides?.templateInputs
10640
+ });
10641
+ toast.dismiss("template-import");
10642
+ if (result.success && result.workflow) {
10643
+ toast.success(tr("fluidgrids.builder.desktopSidebar.workflowCreatedToast", "Workflow created from template!"), { description: `"${result.workflow.name}" is ready to edit` });
10644
+ if ((content.requiredCredentialTypes?.length ?? 0) > 0) toast.info(tr("fluidgrids.builder.desktopSidebar.credentialsNeededToast", "Credentials needed"), {
10645
+ description: `Configure ${content.requiredCredentialTypes?.join(", ")} in the Credentials page`,
10646
+ duration: 6e3
10647
+ });
10648
+ onImported(result.workflow);
10649
+ return true;
10650
+ }
10651
+ if (onQuotaExhausted && isQuotaExhaustedFailure(result.error?.cause ?? result.error)) {
10652
+ onQuotaExhausted();
10653
+ return false;
10654
+ }
10655
+ toast.error(tr("fluidgrids.builder.desktopSidebar.importTemplateErrorToast", "Failed to import template"), { description: result.message || tr("fluidgrids.builder.desktopSidebar.importTemplateErrorFallback", "Unknown error during import") });
10656
+ return false;
10657
+ } catch (err) {
10658
+ toast.dismiss("template-import");
10659
+ toast.error(tr("fluidgrids.builder.desktopSidebar.importFailedToast", "Import failed"), { description: err instanceof Error ? err.message : tr("fluidgrids.builder.desktopSidebar.importFailedFallback", "Unknown error") });
10660
+ return false;
10661
+ } finally {
10662
+ setInstallProgress(null);
10663
+ setBusy(false);
10664
+ }
10665
+ }, [
10666
+ importTemplate,
10667
+ installMissingNodes,
10668
+ onImported,
10669
+ onQuotaExhausted,
10670
+ tr
10671
+ ]);
10672
+ const start = useCallback(async (template) => {
10673
+ setBusy(true);
10674
+ toast.loading(tr("fluidgrids.builder.desktopSidebar.loadingTemplateToast", "Loading template..."), { id: "template-load" });
10675
+ try {
10676
+ const full = await getTemplateDetail(template.id);
10677
+ toast.dismiss("template-load");
10678
+ if (!full) {
10679
+ toast.error(tr("fluidgrids.builder.desktopSidebar.templateDetailsErrorToast", "Failed to load template details"));
10680
+ return;
10681
+ }
10682
+ const content = await resolveTemplateContent(full, buildRequiredNodeTypes);
10683
+ if (!content) {
10684
+ toast.error(tr("fluidgrids.builder.desktopSidebar.invalidTemplateDataToast", "Template does not contain valid workflow data"));
10685
+ return;
10686
+ }
10687
+ const nodeCheck = buildNodeCheck(content.requiredNodeTypes ?? [], installedAppIds);
10688
+ if (needsTemplateConfiguration(content, nodeCheck)) {
10689
+ setPending({
10690
+ template: full,
10691
+ content,
10692
+ nodeCheck
10693
+ });
10694
+ return;
10695
+ }
10696
+ await createCopy(full, content, nodeCheck);
10697
+ } catch (err) {
10698
+ toast.dismiss("template-load");
10699
+ 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") });
10700
+ } finally {
10701
+ setBusy(false);
10702
+ }
10703
+ }, [
10704
+ createCopy,
10705
+ getTemplateDetail,
10706
+ installedAppIds,
10707
+ tr
10708
+ ]);
10709
+ const confirm = useCallback(async (config) => {
10710
+ if (!pending?.nodeCheck.canImport) return;
10711
+ if (await createCopy(pending.template, pending.content, pending.nodeCheck, config)) setPending(null);
10712
+ }, [createCopy, pending]);
10713
+ return {
10714
+ start,
10715
+ dialog: pending ? /* @__PURE__ */ jsx(Suspense, {
10716
+ fallback: null,
10717
+ children: /* @__PURE__ */ jsx(LazyTemplateConfigurationModal, {
10718
+ open: true,
10719
+ onOpenChange: (open) => {
10720
+ if (!open && !busy) setPending(null);
10721
+ },
10722
+ template: pending.template,
10723
+ content: pending.content,
10724
+ nodeCheckResult: pending.nodeCheck,
10725
+ onConfirm: confirm,
10726
+ importing: busy,
10727
+ installProgress
10728
+ })
10729
+ }) : null,
10730
+ busy
10731
+ };
10732
+ }
10733
+
10734
+ // src/hooks/store/useOrganizationPrivateNodes.ts
10735
+ function errorMessage(error, fallback) {
10736
+ return error instanceof Error && error.message ? error.message : fallback;
10737
+ }
10738
+ /**
10739
+ * The workspace-private custom nodes of the current workspace's organization
10740
+ * (BOFF-9766), with install, update and uninstall.
10741
+ *
10742
+ * These nodes skip the public store: devportal's `organizationPrivateNodes`
10743
+ * lists them, `installApp` installs a chosen version (which also registers
10744
+ * it in the workspace's builder; installing a newer version over an active
10745
+ * install updates it), and `uninstallApp` removes it. After every change the
10746
+ * builder's node palette is refreshed so the node appears or disappears.
10747
+ */
10748
+ function useOrganizationPrivateNodes() {
10749
+ const client = useGlobalStoreClient();
10750
+ const workspaceId = useContext(FluidGridsContext)?.workspaceId ?? getFluidGridsConfig().workspaceId;
10751
+ const { refreshAvailableNodes } = useRefreshAvailableNodes();
10752
+ const [busyApplicationId, setBusyApplicationId] = useState(null);
10753
+ const { data, loading, error, refetch } = useQuery(OrganizationPrivateNodesDocument, {
10754
+ client,
10755
+ variables: { workspaceId: workspaceId || "" },
10756
+ skip: !workspaceId,
10757
+ fetchPolicy: "cache-and-network"
10758
+ });
10759
+ const [installMutation] = useMutation(InstallOrganizationPrivateNodeDocument, { client });
10760
+ const [uninstallMutation] = useMutation(UninstallOrganizationPrivateNodeDocument, { client });
10761
+ const afterChange = useCallback(async () => {
10762
+ await refreshAvailableNodes();
10763
+ await refetch();
10764
+ }, [refreshAvailableNodes, refetch]);
10765
+ /** Install the node's latest organization version, or update to it. */
10766
+ const install = useCallback(async (node) => {
10767
+ if (!workspaceId) return {
10768
+ success: false,
10769
+ error: "No workspace"
10770
+ };
10771
+ setBusyApplicationId(node.applicationId);
10772
+ try {
10773
+ const result = await installMutation({ variables: {
10774
+ applicationId: node.applicationId,
10775
+ workspaceId,
10776
+ version: node.version
10777
+ } });
10778
+ if (result.error || !result.data?.installApp) throw result.error ?? /* @__PURE__ */ new Error("Installation failed");
10779
+ await afterChange();
10780
+ return { success: true };
10781
+ } catch (err) {
10782
+ return {
10783
+ success: false,
10784
+ error: errorMessage(err, "Installation failed")
10785
+ };
10786
+ } finally {
10787
+ setBusyApplicationId(null);
10788
+ }
10789
+ }, [
10790
+ afterChange,
10791
+ installMutation,
10792
+ workspaceId
10793
+ ]);
10794
+ const uninstall = useCallback(async (node) => {
10795
+ if (!node.installationId) return {
10796
+ success: false,
10797
+ error: "The node is not installed"
10798
+ };
10799
+ setBusyApplicationId(node.applicationId);
10800
+ try {
10801
+ const result = await uninstallMutation({ variables: { installationId: node.installationId } });
10802
+ if (result.error || result.data?.uninstallApp !== true) throw result.error ?? /* @__PURE__ */ new Error("Uninstall failed");
10803
+ await afterChange();
10804
+ return { success: true };
10805
+ } catch (err) {
10806
+ return {
10807
+ success: false,
10808
+ error: errorMessage(err, "Uninstall failed")
10809
+ };
10810
+ } finally {
10811
+ setBusyApplicationId(null);
10812
+ }
10813
+ }, [afterChange, uninstallMutation]);
10814
+ const listing = data?.organizationPrivateNodes;
10815
+ return {
10816
+ nodes: listing?.nodes ?? [],
10817
+ canManage: listing?.canManage ?? false,
10818
+ loading,
10819
+ error,
10820
+ busyApplicationId,
10821
+ install,
10822
+ uninstall,
10823
+ refetch
10824
+ };
10825
+ }
10826
+
10827
+ // src/builder/components/flow-builder/components/blocks/sidebar/panels/store-browse/components/store-node-list-item.tsx
10828
+ function StoreNodeListItem({ name, description, icon, isInstalled, isPrebuilt, hasUpdate, isDeprecated, nodeType, isDraggable, onClick }) {
10829
+ const tr = useTr();
10830
+ const displayIcon = getStoreNodeIcon(icon, nodeType);
10831
+ const handleDragStart = useCallback((e) => {
10832
+ if (!isDraggable || !nodeType) return;
10833
+ e.dataTransfer.setData(NODE_TYPE_DRAG_DATA_FORMAT, nodeType);
10834
+ e.dataTransfer.effectAllowed = "move";
10835
+ }, [isDraggable, nodeType]);
10836
+ return /* @__PURE__ */ jsxs("button", {
10837
+ onClick,
10838
+ draggable: isDraggable && !!nodeType,
10839
+ onDragStart: handleDragStart,
10840
+ title: isDraggable ? `${name} — drag to add to canvas` : name,
10841
+ className: "w-full flex items-center gap-3 px-2 py-2.5 rounded-md hover:bg-muted/60 transition-colors text-left group",
10842
+ style: isDraggable ? { cursor: "grab" } : void 0,
10843
+ children: [
10844
+ /* @__PURE__ */ jsx("div", {
10845
+ className: "relative size-9 shrink-0 rounded-lg bg-muted flex items-center justify-center overflow-hidden",
10846
+ children: displayIcon ? isImageIcon(displayIcon) ? /* @__PURE__ */ jsx("img", {
10847
+ src: displayIcon,
10848
+ alt: name,
10849
+ className: "w-full h-full object-cover"
10850
+ }) : /* @__PURE__ */ jsx(Icon, {
10851
+ icon: displayIcon,
10852
+ className: "size-5 text-muted-foreground"
10853
+ }) : /* @__PURE__ */ jsx("span", {
10854
+ className: "text-sm font-bold text-muted-foreground",
10855
+ children: name.charAt(0).toUpperCase()
10856
+ })
10857
+ }),
10858
+ /* @__PURE__ */ jsxs("div", {
10859
+ className: "flex-1 min-w-0",
10860
+ children: [/* @__PURE__ */ jsxs("div", {
10861
+ className: "flex items-center gap-1.5 flex-wrap",
10862
+ children: [
10863
+ /* @__PURE__ */ jsx("span", {
10864
+ className: "text-sm font-medium text-foreground group-hover:text-primary transition-colors truncate",
10865
+ children: name
10866
+ }),
10867
+ isDeprecated && /* @__PURE__ */ jsx("span", {
10868
+ title: tr("fluidgrids.builder.storeNodeListItem.deprecatedTooltip", "This node is no longer supported. Consider migrating to a newer alternative."),
10869
+ className: "shrink-0 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-semibold cursor-help",
10870
+ style: {
10871
+ background: "var(--color-status-warning-bgSubtle)",
10872
+ color: "var(--color-status-warning-text)"
10873
+ },
10874
+ children: tr("fluidgrids.builder.storeNodeListItem.deprecatedBadge", "Deprecated")
10875
+ }),
10876
+ hasUpdate && !isDeprecated && /* @__PURE__ */ jsx("span", {
10877
+ className: "shrink-0 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold",
10878
+ style: {
10879
+ background: "var(--color-status-warning-bgSubtle)",
10880
+ color: "var(--color-status-warning-text)"
10881
+ },
10882
+ children: tr("fluidgrids.builder.storeNodeListItem.updateBadge", "Update")
10883
+ }),
10884
+ isInstalled && !hasUpdate && !isDeprecated && /* @__PURE__ */ jsxs("span", {
10885
+ className: "shrink-0 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-semibold",
10886
+ style: {
10887
+ background: "var(--color-status-success-bgSubtle)",
10888
+ color: "var(--color-status-success-text)"
10889
+ },
10890
+ children: [/* @__PURE__ */ jsx("span", {
10891
+ className: "size-1.5 rounded-full inline-block",
10892
+ style: { background: "var(--color-status-success-bg)" }
10893
+ }), tr("fluidgrids.builder.storeNodeListItem.installedBadge", "Installed")]
10894
+ }),
10895
+ isPrebuilt && !isInstalled && !hasUpdate && !isDeprecated && /* @__PURE__ */ jsxs("span", {
10896
+ className: "shrink-0 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-semibold",
10897
+ style: {
10898
+ background: "var(--color-status-info-bgSubtle)",
10899
+ color: "var(--color-status-info-text)"
10900
+ },
10901
+ children: [/* @__PURE__ */ jsx("span", {
10902
+ className: "size-1.5 rounded-full inline-block",
10903
+ style: { background: "var(--color-status-info-bg)" }
10904
+ }), tr("fluidgrids.builder.storeNodeListItem.prebuiltBadge", "Prebuilt")]
10905
+ })
10906
+ ]
10907
+ }), description && /* @__PURE__ */ jsx("p", {
10908
+ className: "text-xs text-muted-foreground truncate mt-0.5 leading-snug",
10909
+ children: description
10910
+ })]
10911
+ }),
10912
+ isDraggable ? /* @__PURE__ */ jsx(Icon, {
10913
+ icon: "mynaui:drag-dots-vertical",
10914
+ className: "size-4 shrink-0 text-foreground/40 group-hover:text-primary transition-colors"
10915
+ }) : /* @__PURE__ */ jsx(Icon, {
10916
+ icon: "mynaui:chevron-right",
10917
+ className: "size-4 shrink-0 text-foreground/40"
10918
+ })
10919
+ ]
10920
+ });
10921
+ }
10922
+
10923
+ // src/builder/components/flow-builder/components/blocks/sidebar/panels/store-browse/components/organization-private-nodes.tsx
10924
+ const KEY = "fluidgrids.builder.organizationPrivateNodes";
10925
+ function NodeIcon({ name, icon }) {
10926
+ return /* @__PURE__ */ jsx("div", {
10927
+ className: "size-9 shrink-0 rounded-lg bg-muted flex items-center justify-center overflow-hidden",
10928
+ children: icon ? isImageIcon(icon) ? /* @__PURE__ */ jsx("img", {
10929
+ src: icon,
10930
+ alt: name,
10931
+ className: "w-full h-full object-cover"
10932
+ }) : /* @__PURE__ */ jsx(Icon, {
10933
+ icon,
10934
+ className: "size-5 text-muted-foreground"
10935
+ }) : /* @__PURE__ */ jsx("span", {
10936
+ className: "text-sm font-bold text-muted-foreground",
10937
+ children: name.charAt(0).toUpperCase()
10938
+ })
10939
+ });
10940
+ }
10941
+ function OrganizationPrivateNodeRow({ node, canManage, busy, disabled, onInstall, onUninstall }) {
10124
10942
  const tr = useTr();
10125
10943
  const [confirmingUninstall, setConfirmingUninstall] = useState(false);
10126
10944
  return /* @__PURE__ */ jsxs("div", {
@@ -11941,7 +12759,7 @@ function TemplatesGrid({ templates, loading, hasMore, onLoadMore, onTemplateClic
11941
12759
  }
11942
12760
 
11943
12761
  // src/builder/components/flow-builder/components/templates/TemplatesModal.tsx
11944
- const LazyTemplateDetailPanel = lazy(() => import("./TemplateDetailPanel-Cpzjif1n.js").then((m) => ({ default: m.TemplateDetailPanel })));
12762
+ const LazyTemplateDetailPanel = lazy(() => import("./TemplateDetailPanel-C0YiVdub.js").then((m) => ({ default: m.TemplateDetailPanel })));
11945
12763
  /**
11946
12764
  * Modal for browsing and selecting workflow templates
11947
12765
  * Features:
@@ -12043,540 +12861,36 @@ function TemplatesModal({ open, onOpenChange, onUseTemplate }) {
12043
12861
  hasMore,
12044
12862
  onLoadMore: loadMore,
12045
12863
  onTemplateClick: handleTemplateClick,
12046
- onUseTemplate: handleUseTemplate
12047
- })
12048
- }), selectedTemplate && /* @__PURE__ */ jsx(Suspense, {
12049
- fallback: /* @__PURE__ */ jsx("div", {
12050
- className: "w-full md:w-1/2 lg:w-2/5 border-l bg-background flex items-center justify-center",
12051
- children: /* @__PURE__ */ jsx(Loader2, { className: "size-6 animate-spin text-muted-foreground" })
12052
- }),
12053
- children: /* @__PURE__ */ jsx(LazyTemplateDetailPanel, {
12054
- template: selectedTemplate,
12055
- onClose: handleCloseDetailPanel,
12056
- onUseTemplate: handleUseTemplate
12057
- })
12058
- })]
12059
- })
12060
- ]
12061
- })
12062
- });
12063
- }
12064
-
12065
- // src/utils/workflow-node-check.ts
12066
- /**
12067
- * Canvas-only node types: they live on the React Flow canvas but are never
12068
- * executed, never registered in NODE_CONFIGS and never published to the store.
12069
- *
12070
- * They must be excluded from the dependency check. A sticky note reaching it
12071
- * was reported as `sticky-note v1.0 — Not installed` in the Missing Nodes
12072
- * dialog on every open of an affected workflow, and because no store product
12073
- * backs it the Install action could only ever be a no-op (BOFF-7267).
12074
- *
12075
- * Kept as a literal set rather than importing STICKY_NOTE_TYPE from the
12076
- * builder so this module stays free of builder-component imports.
12077
- */
12078
- const CANVAS_ONLY_NODE_TYPES = /* @__PURE__ */ new Set(["sticky-note"]);
12079
- /**
12080
- * Resolve the set of unique executable node types used by a workflow.
12081
- *
12082
- * Canvas-only annotations (see CANVAS_ONLY_NODE_TYPES) are skipped: they carry
12083
- * no dependency, so including them produces a permanently unsatisfiable
12084
- * "missing node".
12085
- */
12086
- function getWorkflowNodeTypes(nodes) {
12087
- return [...new Set(nodes.map((n) => n.type).filter((t) => Boolean(t)).filter((t) => !CANVAS_ONLY_NODE_TYPES.has(t)))];
12088
- }
12089
- /**
12090
- * Page size for walking the node catalog. global-store-svc clamps
12091
- * `browseStore` pages to 100 rows (MAX_PAGINATION_LIMIT), so asking for more
12092
- * returns 100 anyway. The old single request for 200 rows silently dropped
12093
- * every node past the first 100 (BOFF-11402).
12094
- */
12095
- const STORE_NODE_PAGE_SIZE = 100;
12096
- /**
12097
- * Safety net for the paging loop: 50 pages of 100 is far beyond the catalog
12098
- * (about 175 node products in 2026-11). The loop normally ends on `hasMore`.
12099
- */
12100
- const STORE_NODE_MAX_PAGES = 50;
12101
- /** The browseStore variables for one page of FluidGrids node products. */
12102
- function storeNodePageVariables(offset) {
12103
- return { input: {
12104
- filter: {
12105
- type: "NODE",
12106
- compatibleWith: "fluidgrids"
12107
- },
12108
- sortBy: "DOWNLOADS",
12109
- pagination: {
12110
- limit: 100,
12111
- offset
12112
- }
12113
- } };
12114
- }
12115
- /**
12116
- * Fetch every FluidGrids-compatible node product from the store.
12117
- *
12118
- * `browseStore` pages by `offset`; its `cursor` input is ignored by the
12119
- * server. The offset advances by the number of rows actually returned so a
12120
- * lower server cap still walks the whole catalog. Rows are de-duplicated by
12121
- * product id in case a page boundary moves while the catalog changes.
12122
- */
12123
- async function fetchAllStoreNodes(fetchPage) {
12124
- const byId = /* @__PURE__ */ new Map();
12125
- let offset = 0;
12126
- for (let page = 0; page < 50; page++) {
12127
- const browse = await fetchPage(storeNodePageVariables(offset));
12128
- const products = browse?.products ?? [];
12129
- for (const product of products) if (!byId.has(product.id)) byId.set(product.id, product);
12130
- if (!browse?.hasMore || products.length === 0) break;
12131
- offset += products.length;
12132
- }
12133
- return [...byId.values()];
12134
- }
12135
- function publishedAtMs(product) {
12136
- const ms = product.publishedAt ? Date.parse(product.publishedAt) : NaN;
12137
- return Number.isNaN(ms) ? Number.NEGATIVE_INFINITY : ms;
12138
- }
12139
- /**
12140
- * Order two store products that offer the same nodeType; a negative result
12141
- * means `a` is the better match.
12142
- *
12143
- * 1. A product linked to a devportal application (`productId` set) wins. Only
12144
- * those have an installable published version; the unlinked rows are
12145
- * orphaned seed rows such as `app-gmail-reader-001` that cannot install.
12146
- * 2. Then more downloads, 3. then the most recently published, 4. then the
12147
- * product id, so the choice never depends on the order the store returned.
12148
- */
12149
- function compareStoreNodeCandidates(a, b) {
12150
- const aLinked = Boolean(a.productId);
12151
- if (aLinked !== Boolean(b.productId)) return aLinked ? -1 : 1;
12152
- const downloads = (b.downloads ?? 0) - (a.downloads ?? 0);
12153
- if (downloads !== 0) return downloads;
12154
- const aPublished = publishedAtMs(a);
12155
- const bPublished = publishedAtMs(b);
12156
- if (aPublished !== bPublished) return aPublished > bPublished ? -1 : 1;
12157
- if (a.id === b.id) return 0;
12158
- return a.id < b.id ? -1 : 1;
12159
- }
12160
- /**
12161
- * Map nodeType -> best store product (see compareStoreNodeCandidates).
12162
- * Pure function, independent of the order of `products`.
12163
- */
12164
- function buildStoreNodeTypeMap(products) {
12165
- const nodeTypeToProduct = /* @__PURE__ */ new Map();
12166
- for (const product of products) {
12167
- const nodeType = product.manifest?.nodeType;
12168
- if (typeof nodeType !== "string" || !nodeType) continue;
12169
- const current = nodeTypeToProduct.get(nodeType);
12170
- if (!current || compareStoreNodeCandidates(product, current) < 0) nodeTypeToProduct.set(nodeType, product);
12171
- }
12172
- return nodeTypeToProduct;
12173
- }
12174
- /**
12175
- * Query the global store for every FluidGrids-compatible node product and map
12176
- * nodeType string -> StoreProduct using manifest.nodeType.
12177
- */
12178
- async function fetchStoreNodeTypeMap() {
12179
- const storeClient = getGlobalStoreClient();
12180
- if (!storeClient) return /* @__PURE__ */ new Map();
12181
- return buildStoreNodeTypeMap(await fetchAllStoreNodes(async (variables) => {
12182
- return (await storeClient.query({
12183
- query: BrowseWorkflowNodesDocument,
12184
- variables,
12185
- fetchPolicy: "cache-first"
12186
- })).data?.browseStore;
12187
- }));
12188
- }
12189
- /**
12190
- * One required-node entry of a template (BOFF-11496): a type the registry
12191
- * already has is BUILTIN at its registry version; any other type is a STORE
12192
- * node at the node's own version from its store product. Versions are
12193
- * canonical, never the product's manifest format version ("1.0").
12194
- */
12195
- function requiredNodeEntry(type, registered, product) {
12196
- if (registered) return {
12197
- nodeType: type,
12198
- nodeVersion: canonicalNodeVersion(registered.version || "1.0.0"),
12199
- publishType: "BUILTIN",
12200
- displayName: type
12201
- };
12202
- return {
12203
- nodeType: type,
12204
- nodeVersion: storeNodeInstallVersion(product?.manifest, product?.manifestVersion),
12205
- publishType: "STORE",
12206
- storeProductId: product?.id,
12207
- displayName: product?.name ?? type,
12208
- requiresPurchase: product !== void 0 && product.pricingModel !== "FREE"
12209
- };
12210
- }
12211
- /**
12212
- * Build the list of required node types for a workflow.
12213
- *
12214
- * - BUILTIN / already-installed types are marked BUILTIN.
12215
- * - Missing types are looked up in the store; if found, they are marked STORE
12216
- * with storeProductId and requiresPurchase.
12217
- * - Missing types not found in the store are still reported as STORE without
12218
- * a storeProductId so the UI can show "not installed".
12219
- */
12220
- async function buildRequiredNodeTypes(nodes) {
12221
- const workflowNodeTypes = getWorkflowNodeTypes(nodes);
12222
- if (workflowNodeTypes.length === 0) return [];
12223
- await ensureNodeTypesLoaded(workflowNodeTypes);
12224
- const missingTypes = workflowNodeTypes.filter((type) => !NODE_CONFIGS.some((c) => c.type === type));
12225
- let nodeTypeToProduct = /* @__PURE__ */ new Map();
12226
- if (missingTypes.length > 0) nodeTypeToProduct = await fetchStoreNodeTypeMap();
12227
- return workflowNodeTypes.map((type) => {
12228
- const registered = NODE_CONFIGS.find((c) => c.type === type);
12229
- return requiredNodeEntry(type, registered ? { version: registered.defaultData.version } : void 0, nodeTypeToProduct.get(type));
12230
- });
12231
- }
12232
- /**
12233
- * Classify required nodes against the set of installed store app IDs.
12234
- *
12235
- * Pure function — easy to unit test.
12236
- */
12237
- function buildNodeCheck(requiredNodeTypes, installedAppIds) {
12238
- const installed = requiredNodeTypes.filter((n) => n.publishType === "BUILTIN" || n.storeProductId && installedAppIds.has(n.storeProductId));
12239
- const notInstalled = requiredNodeTypes.filter((n) => n.publishType !== "BUILTIN" && (!n.storeProductId || !installedAppIds.has(n.storeProductId)));
12240
- const unavailable = notInstalled.filter((n) => !n.storeProductId);
12241
- const installable = notInstalled.filter((n) => !!n.storeProductId);
12242
- const missing = installable.filter((n) => !n.requiresPurchase);
12243
- const missingPaid = installable.filter((n) => !!n.requiresPurchase);
12244
- return {
12245
- installed,
12246
- missing,
12247
- missingPaid,
12248
- unavailable,
12249
- canImport: missingPaid.length === 0
12250
- };
12864
+ onUseTemplate: handleUseTemplate
12865
+ })
12866
+ }), selectedTemplate && /* @__PURE__ */ jsx(Suspense, {
12867
+ fallback: /* @__PURE__ */ jsx("div", {
12868
+ className: "w-full md:w-1/2 lg:w-2/5 border-l bg-background flex items-center justify-center",
12869
+ children: /* @__PURE__ */ jsx(Loader2, { className: "size-6 animate-spin text-muted-foreground" })
12870
+ }),
12871
+ children: /* @__PURE__ */ jsx(LazyTemplateDetailPanel, {
12872
+ template: selectedTemplate,
12873
+ onClose: handleCloseDetailPanel,
12874
+ onUseTemplate: handleUseTemplate
12875
+ })
12876
+ })]
12877
+ })
12878
+ ]
12879
+ })
12880
+ });
12251
12881
  }
12252
12882
 
12253
12883
  // src/builder/components/flow-builder/components/blocks/sidebar/fragments/desktop-sidebar-fragment.tsx
12254
- const LazyTemplateConfigurationModal = lazy(() => import("./TemplateConfigurationModal-DwJi6alN.js").then((m) => ({ default: m.TemplateConfigurationModal })));
12255
12884
  function DesktopSidebarFragment({ activePanel, setActivePanel }) {
12256
12885
  const tr = useTr();
12257
- const { bus, workspaceId } = useFluidGridsAnalytics();
12258
12886
  const [templatesModalOpen, setTemplatesModalOpen] = useState(false);
12259
- const [configModalOpen, setConfigModalOpen] = useState(false);
12260
- const [configTemplate, setConfigTemplate] = useState(null);
12261
- const [configContent, setConfigContent] = useState(null);
12262
- const [configNodeCheck, setConfigNodeCheck] = useState(null);
12263
- const [importing, setImporting] = useState(false);
12264
- const [installProgress, setInstallProgress] = useState(null);
12265
12887
  const navigate = useGuardedNavigate();
12266
- const { installedAppIds } = useInstalledNodes();
12267
- /**
12268
- * Build the node check result from template content + currently installed nodes.
12269
- * Uses real installed-app IDs to classify nodes as installed / missing / missingPaid.
12270
- */
12271
- const buildNodeCheck = useCallback((content) => {
12272
- const allNodes = content.requiredNodeTypes || [];
12273
- const installed = allNodes.filter((n) => n.publishType === "BUILTIN" || n.storeProductId && installedAppIds.has(n.storeProductId));
12274
- const notInstalled = allNodes.filter((n) => n.publishType !== "BUILTIN" && (!n.storeProductId || !installedAppIds.has(n.storeProductId)));
12275
- const unavailable = notInstalled.filter((n) => !n.storeProductId);
12276
- const installable = notInstalled.filter((n) => !!n.storeProductId);
12277
- const missing = installable.filter((n) => !n.requiresPurchase);
12278
- const missingPaid = installable.filter((n) => !!n.requiresPurchase);
12279
- return {
12280
- installed,
12281
- missing,
12282
- missingPaid,
12283
- unavailable,
12284
- canImport: missingPaid.length === 0
12285
- };
12286
- }, [installedAppIds]);
12287
- /**
12288
- * Perform the actual import of a template.
12289
- *
12290
- * Flow:
12291
- * 1. Install missing free nodes one-by-one (with progress updates)
12292
- * 2. Create the workflow via importWorkflowTemplate mutation
12293
- * 3. Record template installation in the store
12294
- * 4. Navigate to the new workflow in the builder
12295
- */
12296
- const performImport = useCallback(async (template, content, overrides, nodeCheck) => {
12297
- try {
12298
- setImporting(true);
12299
- const nodesToInstall = (nodeCheck?.missing ?? []).filter((n) => !!n.storeProductId);
12300
- if (nodesToInstall.length > 0) {
12301
- const { getGlobalStoreClient: getStore } = await import("./FluidGridsProvider-DpTTWDNQ.js").then((n) => n.dr);
12302
- const { InstallWorkflowNodeDocument } = await import("./FluidGridsProvider-DpTTWDNQ.js").then((n) => n._);
12303
- const { getFluidGridsConfig } = await import("./node-endpoint-configs-DVo9nUOe.js").then((n) => n.qi);
12304
- const storeClient = getStore();
12305
- const cfg = getFluidGridsConfig();
12306
- for (let i = 0; i < nodesToInstall.length; i++) {
12307
- const node = nodesToInstall[i];
12308
- setInstallProgress({
12309
- current: i + 1,
12310
- total: nodesToInstall.length,
12311
- nodeName: node.displayName || node.nodeType
12312
- });
12313
- try {
12314
- if (storeClient) {
12315
- throwIfResultError(await storeClient.mutate({
12316
- mutation: InstallWorkflowNodeDocument,
12317
- variables: { input: {
12318
- productId: node.storeProductId,
12319
- workspaceId: cfg.workspaceId,
12320
- organizationId: cfg.tenantId,
12321
- version: node.nodeVersion,
12322
- manifest: null
12323
- } }
12324
- }));
12325
- emitNodeInstalled(bus, {
12326
- workspaceId,
12327
- nodeType: node.nodeType,
12328
- source: "store"
12329
- });
12330
- }
12331
- } catch (nodeErr) {
12332
- console.warn(`[performImport] Failed to install node ${node.nodeType}:`, nodeErr);
12333
- }
12334
- }
12335
- setInstallProgress(null);
12336
- }
12337
- toast.loading(tr("fluidgrids.builder.desktopSidebar.creatingWorkflowToast", "Creating workflow..."), { id: "template-import" });
12338
- const rawDef = JSON.parse(JSON.stringify(content.definitionJson));
12339
- const nestedDef = rawDef?.data?.workflowDefinition?.definitionJson;
12340
- let definitionJson = nestedDef && typeof nestedDef === "object" ? nestedDef : rawDef;
12341
- if (overrides?.templateInputs) {
12342
- const { applyTemplateInputs } = await import("./template-input-applicator-C0p6hE4K.js").then((n) => n.r);
12343
- definitionJson = applyTemplateInputs(definitionJson, overrides.templateInputs);
12344
- }
12345
- const { getFluidGridsClient } = await import("./node-endpoint-configs-DVo9nUOe.js").then((n) => n.Di);
12346
- const { getFluidGridsConfig } = await import("./node-endpoint-configs-DVo9nUOe.js").then((n) => n.qi);
12347
- const { gql } = await import("@apollo/client/core");
12348
- const IMPORT_MUTATION = gql`
12349
- mutation ImportWorkflowTemplate(
12350
- $input: ImportWorkflowTemplateInput!
12351
- ) {
12352
- importWorkflowTemplate(input: $input) {
12353
- success
12354
- message
12355
- workflow {
12356
- id
12357
- workflowKey
12358
- version
12359
- name
12360
- status
12361
- }
12362
- }
12363
- }
12364
- `;
12365
- const config = getFluidGridsConfig();
12366
- const client = getFluidGridsClient();
12367
- const result = throwIfResultError(await client.mutate({
12368
- mutation: IMPORT_MUTATION,
12369
- variables: { input: {
12370
- definitionJson,
12371
- name: overrides?.name || content.defaultWorkflowName || template.name,
12372
- description: template.description,
12373
- sourceTemplateId: template.id,
12374
- sourceTemplateName: template.name,
12375
- tags: content.defaultTags || template.tags || [],
12376
- category: content.defaultCategory || template.category || void 0,
12377
- icon: template.icon || void 0,
12378
- context: {
12379
- ...config.productId ? { productId: config.productId } : {},
12380
- ...config.productName ? { productName: config.productName } : {}
12381
- }
12382
- } }
12383
- }));
12384
- toast.dismiss("template-import");
12385
- const importResult = result.data?.importWorkflowTemplate;
12386
- if (importResult?.success && importResult.workflow) {
12387
- emitWorkflowCreated(bus, {
12388
- workspaceId,
12389
- source: "template",
12390
- definition: definitionJson
12391
- });
12392
- emitTemplateUsed(bus, {
12393
- workspaceId,
12394
- templateId: template.id,
12395
- category: content.defaultCategory || template.category
12396
- });
12397
- try {
12398
- const { getGlobalStoreClient: getStoreClient } = await import("./FluidGridsProvider-DpTTWDNQ.js").then((n) => n.dr);
12399
- const { InstallWorkflowNodeDocument } = await import("./FluidGridsProvider-DpTTWDNQ.js").then((n) => n._);
12400
- const storeClient = getStoreClient();
12401
- if (storeClient) throwIfResultError(await storeClient.mutate({
12402
- mutation: InstallWorkflowNodeDocument,
12403
- variables: { input: {
12404
- productId: template.id,
12405
- workspaceId: config.workspaceId,
12406
- organizationId: config.tenantId,
12407
- version: template.manifestVersion || "1.0.0",
12408
- manifest: template.manifest,
12409
- metadata: {
12410
- type: "WORKFLOW",
12411
- sourceTemplateId: template.id,
12412
- sourceTemplateName: template.name,
12413
- importedWorkflowKey: importResult.workflow.workflowKey,
12414
- importedWorkflowVersion: importResult.workflow.version
12415
- }
12416
- } }
12417
- }));
12418
- } catch {
12419
- console.warn("[performImport] Failed to record template installation");
12420
- }
12421
- toast.success(tr("fluidgrids.builder.desktopSidebar.workflowCreatedToast", "Workflow created from template!"), { description: `"${importResult.workflow.name}" is ready to edit` });
12422
- if (content.requiredCredentialTypes && content.requiredCredentialTypes.length > 0) toast.info(tr("fluidgrids.builder.desktopSidebar.credentialsNeededToast", "Credentials needed"), {
12423
- description: `Configure ${content.requiredCredentialTypes.join(", ")} in the Credentials page`,
12424
- duration: 6e3
12425
- });
12426
- navigate(`../workflow/${importResult.workflow.workflowKey}/${importResult.workflow.version}`);
12427
- } else toast.error(tr("fluidgrids.builder.desktopSidebar.importTemplateErrorToast", "Failed to import template"), { description: importResult?.message || tr("fluidgrids.builder.desktopSidebar.importTemplateErrorFallback", "Unknown error during import") });
12428
- } catch (err) {
12429
- toast.dismiss("template-import");
12430
- setInstallProgress(null);
12431
- toast.error(tr("fluidgrids.builder.desktopSidebar.importFailedToast", "Import failed"), { description: err instanceof Error ? err.message : tr("fluidgrids.builder.desktopSidebar.importFailedFallback", "Unknown error") });
12432
- } finally {
12433
- setImporting(false);
12434
- }
12435
- }, [
12436
- navigate,
12437
- tr,
12438
- bus,
12439
- workspaceId
12440
- ]);
12441
- /**
12442
- * Handle "Use Template" click from the TemplatesModal.
12443
- *
12444
- * Flow:
12445
- * 1. Fetch full template detail from the store
12446
- * 2. Parse content, check node dependencies against installed nodes
12447
- * 3. If template has required inputs, missing nodes, or credentials -> show config modal
12448
- * 4. Otherwise -> import directly
12449
- */
12888
+ const templateImport = useTemplateImportFlow({ onImported: (workflow) => navigate(`../workflow/${workflow.workflowKey}/${workflow.version}`) });
12889
+ const startTemplateImport = templateImport.start;
12450
12890
  const handleUseTemplate = useCallback(async (template) => {
12451
- try {
12452
- toast.loading(tr("fluidgrids.builder.desktopSidebar.loadingTemplateToast", "Loading template..."), { id: "template-load" });
12453
- const { getGlobalStoreClient } = await import("./FluidGridsProvider-DpTTWDNQ.js").then((n) => n.dr);
12454
- const { gql } = await import("@apollo/client/core");
12455
- const GET_TEMPLATE_DETAIL = gql`
12456
- query GetWorkflowTemplateDetail($id: ID!) {
12457
- product(id: $id) {
12458
- id
12459
- name
12460
- slug
12461
- type
12462
- description
12463
- longDescription
12464
- icon
12465
- screenshots
12466
- rating
12467
- reviewCount
12468
- downloads
12469
- viewCount
12470
- price
12471
- pricingModel
12472
- currency
12473
- featured
12474
- content
12475
- manifest
12476
- manifestVersion
12477
- category
12478
- tags
12479
- compatibleProducts
12480
- authorName
12481
- publisher {
12482
- id
12483
- name
12484
- isVerified
12485
- }
12486
- createdAt
12487
- publishedAt
12488
- }
12489
- }
12490
- `;
12491
- const client = getGlobalStoreClient();
12492
- if (!client) {
12493
- toast.dismiss("template-load");
12494
- toast.error(tr("fluidgrids.builder.desktopSidebar.storeClientUnavailableToast", "Store client not available"));
12495
- return;
12496
- }
12497
- const result = await client.query({
12498
- query: GET_TEMPLATE_DETAIL,
12499
- variables: { id: template.id },
12500
- fetchPolicy: "network-only"
12501
- });
12502
- toast.dismiss("template-load");
12503
- const fullTemplate = result.data?.product ?? null;
12504
- if (!fullTemplate) {
12505
- toast.error(tr("fluidgrids.builder.desktopSidebar.templateDetailsErrorToast", "Failed to load template details"));
12506
- return;
12507
- }
12508
- let content = fullTemplate.content;
12509
- if (!content?.definitionJson) {
12510
- const m = fullTemplate.manifest;
12511
- let nodes;
12512
- let edges = [];
12513
- let workflowName;
12514
- if (m) {
12515
- if (Array.isArray(m.nodes)) {
12516
- nodes = m.nodes;
12517
- edges = m.edges ?? [];
12518
- workflowName = m.name;
12519
- } else if (typeof m.jsonContent === "string") try {
12520
- const parsed = JSON.parse(m.jsonContent);
12521
- if (Array.isArray(parsed.nodes)) {
12522
- nodes = parsed.nodes;
12523
- edges = parsed.edges ?? [];
12524
- workflowName = parsed.name;
12525
- }
12526
- } catch {}
12527
- }
12528
- if (nodes) {
12529
- let requiredNodeTypes = [];
12530
- try {
12531
- requiredNodeTypes = await buildRequiredNodeTypes(nodes);
12532
- } catch {}
12533
- content = {
12534
- definitionJson: {
12535
- nodes,
12536
- edges
12537
- },
12538
- requiredNodeTypes,
12539
- defaultWorkflowName: workflowName ?? fullTemplate.name
12540
- };
12541
- } else {
12542
- toast.error(tr("fluidgrids.builder.desktopSidebar.invalidTemplateDataToast", "Template does not contain valid workflow data"));
12543
- return;
12544
- }
12545
- }
12546
- const nodeCheck = buildNodeCheck(content);
12547
- if (content.requiredInputs && content.requiredInputs.length > 0 || nodeCheck.missing.length > 0 || nodeCheck.missingPaid.length > 0 || (nodeCheck.unavailable?.length ?? 0) > 0 || content.requiredCredentialTypes && content.requiredCredentialTypes.length > 0) {
12548
- setConfigTemplate(fullTemplate);
12549
- setConfigContent(content);
12550
- setConfigNodeCheck(nodeCheck);
12551
- setConfigModalOpen(true);
12552
- setTemplatesModalOpen(false);
12553
- } else {
12554
- setTemplatesModalOpen(false);
12555
- await performImport(fullTemplate, content, void 0, nodeCheck);
12556
- }
12557
- } catch (err) {
12558
- toast.dismiss("template-load");
12559
- 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") });
12560
- }
12561
- }, [
12562
- buildNodeCheck,
12563
- performImport,
12564
- tr
12565
- ]);
12566
- /**
12567
- * Handle confirm from the TemplateConfigurationModal.
12568
- */
12569
- const handleConfigConfirm = useCallback(async (config) => {
12570
- if (!configTemplate || !configContent) return;
12571
- if (configNodeCheck && !configNodeCheck.canImport) return;
12572
- await performImport(configTemplate, configContent, config, configNodeCheck);
12573
- setConfigModalOpen(false);
12574
- }, [
12575
- configTemplate,
12576
- configContent,
12577
- configNodeCheck,
12578
- performImport
12579
- ]);
12891
+ setTemplatesModalOpen(false);
12892
+ await startTemplateImport(template);
12893
+ }, [startTemplateImport]);
12580
12894
  return /* @__PURE__ */ jsxs("div", {
12581
12895
  className: "relative w-fit flex shrink-0 divide-x divide-card-foreground/10 rounded-xl border border-card-foreground/10 md:overflow-hidden md:max-w-md",
12582
12896
  children: [
@@ -12623,19 +12937,7 @@ function DesktopSidebarFragment({ activePanel, setActivePanel }) {
12623
12937
  onOpenChange: setTemplatesModalOpen,
12624
12938
  onUseTemplate: handleUseTemplate
12625
12939
  }),
12626
- configModalOpen && /* @__PURE__ */ jsx(Suspense, {
12627
- fallback: null,
12628
- children: /* @__PURE__ */ jsx(LazyTemplateConfigurationModal, {
12629
- open: configModalOpen,
12630
- onOpenChange: setConfigModalOpen,
12631
- template: configTemplate,
12632
- content: configContent,
12633
- nodeCheckResult: configNodeCheck,
12634
- onConfirm: handleConfigConfirm,
12635
- importing,
12636
- installProgress
12637
- })
12638
- })
12940
+ templateImport.dialog
12639
12941
  ]
12640
12942
  });
12641
12943
  }
@@ -20400,6 +20702,448 @@ function WebhookRunFollower() {
20400
20702
  return null;
20401
20703
  }
20402
20704
 
20705
+ // src/agentMarks/plainText.ts
20706
+ /**
20707
+ * ★ BOFF-11584: the text of a callout or note is model output, and the model
20708
+ * has read workflow content people wrote. It is shown as plain text only:
20709
+ * React renders it as a text node (never `dangerouslySetInnerHTML`, never a
20710
+ * markdown renderer), so markup, links and images in it stay inert characters.
20711
+ *
20712
+ * This keeps it short and printable as well: control characters (other than
20713
+ * line breaks) and bidirectional controls, which can disguise what a line
20714
+ * says, are removed; runs of blank lines collapse; the result is clamped.
20715
+ */
20716
+ const BIDI_CONTROLS = /* @__PURE__ */ new Set([
20717
+ 1564,
20718
+ 8206,
20719
+ 8207,
20720
+ 8234,
20721
+ 8235,
20722
+ 8236,
20723
+ 8237,
20724
+ 8238,
20725
+ 8294,
20726
+ 8295,
20727
+ 8296,
20728
+ 8297
20729
+ ]);
20730
+ function isDroppedCharacter(code) {
20731
+ if (code === 10) return false;
20732
+ if (code <= 31) return true;
20733
+ if (code >= 127 && code <= 159) return true;
20734
+ return BIDI_CONTROLS.has(code);
20735
+ }
20736
+ function toPlainText(text, max) {
20737
+ let kept = "";
20738
+ for (const character of text.replace(/\r\n?/g, "\n").replace(/\t/g, " ")) if (!isDroppedCharacter(character.codePointAt(0) ?? 0)) kept += character;
20739
+ const cleaned = kept.replace(/ {2,}/g, " ").replace(/ *\n */g, "\n").replace(/\n{3,}/g, "\n\n").trim();
20740
+ if (cleaned.length <= max) return cleaned;
20741
+ return `${cleaned.slice(0, max - 1).trimEnd()}…`;
20742
+ }
20743
+
20744
+ // src/agentMarks/marksStore.ts
20745
+ /** The text of a mark is model output: kept as plain text, never markup. */
20746
+ const CALLOUT_LABEL_MAX = 200;
20747
+ const NOTE_MESSAGE_MAX = 280;
20748
+ /** A workflow holds at most this many notes, resolved ones included. */
20749
+ const MAX_NOTES_PER_WORKFLOW = 20;
20750
+ let sequence = 0;
20751
+ const nextId = (prefix) => {
20752
+ sequence += 1;
20753
+ return `${prefix}${sequence}`;
20754
+ };
20755
+ const useAgentMarksStore = create()((set, get) => ({
20756
+ callouts: [],
20757
+ notes: [],
20758
+ nextNumber: {},
20759
+ openNoteId: null,
20760
+ flash: null,
20761
+ actions: {
20762
+ addCallout: ({ label, target, element, route }) => {
20763
+ const callout = {
20764
+ id: nextId("c"),
20765
+ label: toPlainText(label, 200),
20766
+ target,
20767
+ route,
20768
+ element: element ? new WeakRef(element) : null
20769
+ };
20770
+ set((state) => ({ callouts: [...state.callouts, callout].slice(-3) }));
20771
+ return callout;
20772
+ },
20773
+ rebindCallout: (id, element) => set((state) => ({ callouts: state.callouts.map((callout) => callout.id === id ? {
20774
+ ...callout,
20775
+ element: new WeakRef(element)
20776
+ } : callout) })),
20777
+ removeCallout: (id) => set((state) => ({ callouts: state.callouts.filter((callout) => callout.id !== id) })),
20778
+ clearCallouts: () => {
20779
+ const count = get().callouts.length;
20780
+ if (count > 0) set({ callouts: [] });
20781
+ return count;
20782
+ },
20783
+ dropCalloutsOffRoute: (route) => {
20784
+ const { callouts } = get();
20785
+ const kept = callouts.filter((callout) => callout.route === route);
20786
+ if (kept.length !== callouts.length) set({ callouts: kept });
20787
+ return callouts.length - kept.length;
20788
+ },
20789
+ addNote: (input) => {
20790
+ const { notes, nextNumber } = get();
20791
+ if (notes.filter((note) => note.workflowKey === input.workflowKey).length >= 20) return null;
20792
+ const number = nextNumber[input.workflowKey] ?? 1;
20793
+ const note = {
20794
+ id: nextId("n"),
20795
+ number,
20796
+ workflowKey: input.workflowKey,
20797
+ nodeId: input.nodeId,
20798
+ nodeLabel: toPlainText(input.nodeLabel, 80),
20799
+ paramKey: input.paramKey ?? null,
20800
+ kind: input.kind,
20801
+ message: toPlainText(input.message, 280),
20802
+ resolved: false
20803
+ };
20804
+ set({
20805
+ notes: [...notes, note],
20806
+ nextNumber: {
20807
+ ...nextNumber,
20808
+ [input.workflowKey]: number + 1
20809
+ }
20810
+ });
20811
+ return note;
20812
+ },
20813
+ setResolved: (id, resolved) => set((state) => ({ notes: state.notes.map((note) => note.id === id ? {
20814
+ ...note,
20815
+ resolved
20816
+ } : note) })),
20817
+ removeNote: (id) => set((state) => ({
20818
+ notes: state.notes.filter((note) => note.id !== id),
20819
+ openNoteId: state.openNoteId === id ? null : state.openNoteId
20820
+ })),
20821
+ clearNotes: (workflowKey) => {
20822
+ const { notes, openNoteId } = get();
20823
+ const kept = workflowKey === void 0 ? [] : notes.filter((note) => note.workflowKey !== workflowKey);
20824
+ const removed = notes.length - kept.length;
20825
+ if (removed > 0) set({
20826
+ notes: kept,
20827
+ openNoteId: kept.some((note) => note.id === openNoteId) ? openNoteId : null
20828
+ });
20829
+ return removed;
20830
+ },
20831
+ openNote: (id) => set({ openNoteId: id }),
20832
+ flashNode: (nodeId) => {
20833
+ sequence += 1;
20834
+ set({ flash: {
20835
+ nodeId,
20836
+ token: sequence
20837
+ } });
20838
+ }
20839
+ }
20840
+ }));
20841
+ /** Reset everything (tests, and sign-out if a host wants it). */
20842
+ function resetAgentMarks() {
20843
+ useAgentMarksStore.setState({
20844
+ callouts: [],
20845
+ notes: [],
20846
+ nextNumber: {},
20847
+ openNoteId: null,
20848
+ flash: null
20849
+ });
20850
+ }
20851
+
20852
+ // src/builder/agentNotes/notesKey.ts
20853
+ /**
20854
+ * ★ BOFF-11584: which workflow a canvas note belongs to. A saved workflow is
20855
+ * known by its key across versions; a new one has no key until its first
20856
+ * save, so its in-memory id stands in.
20857
+ */
20858
+ function notesKeyOf(workflow) {
20859
+ return workflow.workflowKey || workflow.id || "unsaved";
20860
+ }
20861
+
20862
+ // src/builder/agentNotes/CanvasNotesLayer.tsx
20863
+ /**
20864
+ * ★ BOFF-11584: the assistant's notes on the builder canvas
20865
+ * (`fg_canvas_annotate`). Rendered as a child of `<ReactFlow>`, outside every
20866
+ * node component: the nodes know nothing about notes.
20867
+ *
20868
+ * - Numbered pins sit above their node (`NodeToolbar`), so they follow it
20869
+ * through drags, pans and zooms. A pin opens the note's card: the message,
20870
+ * the parameter it is about, "Open node" and "Mark resolved".
20871
+ * - A dashed ring marks each node with an open note (`ViewportPortal`, in
20872
+ * canvas coordinates), and a brief ring shows the node the assistant just
20873
+ * brought into view.
20874
+ * - The notes list (top left; the FluidGrids mark holds the top right)
20875
+ * holds every note, including orphaned ones whose node was deleted: those
20876
+ * are never moved onto another node.
20877
+ *
20878
+ * Everything here is `data-agent="deny"`: an agent cannot read or press its
20879
+ * own notes. Messages render as React text, never as markup.
20880
+ */
20881
+ const FLASH_MS = 1800;
20882
+ const RING_PAD = 8;
20883
+ function pinClass(note, open) {
20884
+ return cn("flex size-6 items-center justify-center rounded-full text-xs font-semibold shadow-md transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", note.resolved ? "bg-muted text-muted-foreground" : note.kind === "fix" ? "bg-destructive text-destructive-foreground" : "bg-primary text-primary-foreground", open && "scale-110 ring-2 ring-ring ring-offset-1 ring-offset-background");
20885
+ }
20886
+ /** A ring around a node, drawn in canvas coordinates. */
20887
+ const NodeRing = ({ nodeId, tone }) => {
20888
+ const node = useInternalNode(nodeId);
20889
+ const width = node?.measured.width ?? 0;
20890
+ const height = node?.measured.height ?? 0;
20891
+ if (!node || width === 0 || height === 0) return null;
20892
+ const { x, y } = node.internals.positionAbsolute;
20893
+ return /* @__PURE__ */ jsx("div", {
20894
+ "aria-hidden": "true",
20895
+ "data-testid": `fg-canvas-note-ring-${tone}`,
20896
+ className: cn("pointer-events-none absolute left-0 top-0 rounded-2xl border-2", tone === "fix" && "border-dashed border-destructive", tone === "info" && "border-dashed border-primary", tone === "flash" && "border-primary ring-4 ring-primary/30 motion-safe:animate-pulse"),
20897
+ style: {
20898
+ transform: `translate(${x - RING_PAD}px, ${y - RING_PAD}px)`,
20899
+ width: width + 16,
20900
+ height: height + 16
20901
+ }
20902
+ });
20903
+ };
20904
+ /** The node the assistant just brought into view, for a moment. */
20905
+ const FlashRing = () => {
20906
+ const flash = useAgentMarksStore((state) => state.flash);
20907
+ const [shown, setShown] = useState(null);
20908
+ useEffect(() => {
20909
+ if (!flash) return;
20910
+ setShown(flash.token);
20911
+ const timer = setTimeout(() => setShown(null), FLASH_MS);
20912
+ return () => clearTimeout(timer);
20913
+ }, [flash]);
20914
+ if (!flash || shown !== flash.token) return null;
20915
+ return /* @__PURE__ */ jsx(NodeRing, {
20916
+ nodeId: flash.nodeId,
20917
+ tone: "flash"
20918
+ });
20919
+ };
20920
+ const CanvasNoteCard = ({ note, nodeOnCanvas }) => {
20921
+ const tr = useTr();
20922
+ const { openNote, setResolved } = useAgentMarksStore((state) => state.actions);
20923
+ const openNodePanel = () => {
20924
+ const { workflow, actions } = useFlowStore.getState();
20925
+ const node = workflow.nodes.find((candidate) => candidate.id === note.nodeId);
20926
+ if (!node?.type) return;
20927
+ actions.ndv.open({
20928
+ nodeId: node.id,
20929
+ type: node.type
20930
+ });
20931
+ openNote(null);
20932
+ };
20933
+ return /* @__PURE__ */ jsxs("div", {
20934
+ "data-testid": "fg-canvas-note-card",
20935
+ className: "nowheel w-64 cursor-default rounded-lg border border-border bg-popover p-3 text-left text-xs text-foreground shadow-lg",
20936
+ children: [
20937
+ /* @__PURE__ */ jsxs("div", {
20938
+ className: "mb-1.5 flex items-center justify-between gap-2",
20939
+ children: [/* @__PURE__ */ jsxs("span", {
20940
+ className: "flex items-center gap-1.5 font-semibold",
20941
+ children: [/* @__PURE__ */ jsx(Sparkles, {
20942
+ "aria-hidden": "true",
20943
+ className: "size-3.5 text-primary"
20944
+ }), note.kind === "fix" ? tr("fluidgrids.agentMarks.noteFixTitle", "Note {{number}} · To fix", { number: note.number }) : tr("fluidgrids.agentMarks.noteInfoTitle", "Note {{number}}", { number: note.number })]
20945
+ }), /* @__PURE__ */ jsx("button", {
20946
+ type: "button",
20947
+ "aria-label": tr("fluidgrids.agentMarks.closeNote", "Close note"),
20948
+ onClick: () => openNote(null),
20949
+ className: "-m-1 rounded p-1 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
20950
+ children: /* @__PURE__ */ jsx(X, {
20951
+ "aria-hidden": "true",
20952
+ className: "size-3.5"
20953
+ })
20954
+ })]
20955
+ }),
20956
+ /* @__PURE__ */ jsxs("p", {
20957
+ className: "mb-1 truncate text-muted-foreground",
20958
+ children: [note.nodeLabel, note.paramKey ? ` · ${tr("fluidgrids.agentMarks.noteParam", "Field: {{param}}", { param: note.paramKey })}` : ""]
20959
+ }),
20960
+ /* @__PURE__ */ jsx("p", {
20961
+ "data-testid": "fg-canvas-note-message",
20962
+ className: "whitespace-pre-line break-words text-sm text-foreground",
20963
+ children: note.message
20964
+ }),
20965
+ !nodeOnCanvas && /* @__PURE__ */ jsx("p", {
20966
+ className: "mt-1.5 text-muted-foreground",
20967
+ children: tr("fluidgrids.agentMarks.noteOrphaned", "This node was removed from the workflow.")
20968
+ }),
20969
+ /* @__PURE__ */ jsxs("div", {
20970
+ className: "mt-2.5 flex justify-end gap-2",
20971
+ children: [nodeOnCanvas && /* @__PURE__ */ jsx("button", {
20972
+ type: "button",
20973
+ onClick: openNodePanel,
20974
+ className: "rounded-md border border-border px-2 py-1 font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
20975
+ children: tr("fluidgrids.agentMarks.openNode", "Open node")
20976
+ }), /* @__PURE__ */ jsx("button", {
20977
+ type: "button",
20978
+ "data-testid": "fg-canvas-note-resolve",
20979
+ onClick: () => setResolved(note.id, !note.resolved),
20980
+ className: "rounded-md bg-primary px-2 py-1 font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
20981
+ children: note.resolved ? tr("fluidgrids.agentMarks.reopenNote", "Reopen") : tr("fluidgrids.agentMarks.resolveNote", "Mark resolved")
20982
+ })]
20983
+ })
20984
+ ]
20985
+ });
20986
+ };
20987
+ /** The pins above one node, and the open card. */
20988
+ const NodePins = ({ nodeId, notes }) => {
20989
+ const tr = useTr();
20990
+ const openNoteId = useAgentMarksStore((state) => state.openNoteId);
20991
+ const openNote = useAgentMarksStore((state) => state.actions.openNote);
20992
+ const open = notes.find((note) => note.id === openNoteId);
20993
+ return /* @__PURE__ */ jsx(NodeToolbar, {
20994
+ nodeId,
20995
+ isVisible: true,
20996
+ position: Position.Top,
20997
+ align: "end",
20998
+ offset: 6,
20999
+ "data-agent": "deny",
21000
+ "data-testid": "fg-canvas-note-pins",
21001
+ className: "nodrag nopan",
21002
+ children: /* @__PURE__ */ jsxs("div", {
21003
+ className: "flex flex-col items-end gap-1.5",
21004
+ children: [open && /* @__PURE__ */ jsx(CanvasNoteCard, {
21005
+ note: open,
21006
+ nodeOnCanvas: true
21007
+ }), /* @__PURE__ */ jsx("div", {
21008
+ className: "flex gap-1",
21009
+ children: notes.map((note) => /* @__PURE__ */ jsx("button", {
21010
+ type: "button",
21011
+ "data-testid": "fg-canvas-note-pin",
21012
+ "aria-expanded": note.id === openNoteId,
21013
+ "aria-label": tr("fluidgrids.agentMarks.notePin", "Assistant note {{number}}", { number: note.number }),
21014
+ onClick: () => openNote(note.id === openNoteId ? null : note.id),
21015
+ className: pinClass(note, note.id === openNoteId),
21016
+ children: note.resolved ? /* @__PURE__ */ jsx(Check, {
21017
+ "aria-hidden": "true",
21018
+ className: "size-3.5"
21019
+ }) : note.number
21020
+ }, note.id))
21021
+ })]
21022
+ })
21023
+ });
21024
+ };
21025
+ /** Every note on this workflow, orphaned ones included. */
21026
+ const NotesList = ({ notes, live }) => {
21027
+ const tr = useTr();
21028
+ const [expanded, setExpanded] = useState(false);
21029
+ const { fitView } = useReactFlow();
21030
+ const { openNote, removeNote, flashNode } = useAgentMarksStore((state) => state.actions);
21031
+ const openCount = notes.filter((note) => !note.resolved).length;
21032
+ const orphaned = notes.filter((note) => !live.has(note.nodeId)).length;
21033
+ const show = (note) => {
21034
+ if (!live.has(note.nodeId)) return;
21035
+ fitView({
21036
+ nodes: [{ id: note.nodeId }],
21037
+ duration: 400,
21038
+ padding: .6,
21039
+ maxZoom: 1.25
21040
+ }).then((done) => {
21041
+ if (done) flashNode(note.nodeId);
21042
+ });
21043
+ openNote(note.id);
21044
+ };
21045
+ return /* @__PURE__ */ jsx(Panel, {
21046
+ position: "top-left",
21047
+ "data-agent": "deny",
21048
+ "data-testid": "fg-canvas-notes-list",
21049
+ className: "nodrag nopan nowheel",
21050
+ children: /* @__PURE__ */ jsxs("div", {
21051
+ className: "w-64 rounded-lg border border-border bg-popover text-xs text-foreground shadow-md",
21052
+ children: [/* @__PURE__ */ jsxs("button", {
21053
+ type: "button",
21054
+ "aria-expanded": expanded,
21055
+ onClick: () => setExpanded((value) => !value),
21056
+ className: "flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
21057
+ children: [/* @__PURE__ */ jsxs("span", {
21058
+ className: "flex items-center gap-1.5",
21059
+ children: [/* @__PURE__ */ jsx(Sparkles, {
21060
+ "aria-hidden": "true",
21061
+ className: "size-3.5 text-primary"
21062
+ }), tr("fluidgrids.agentMarks.notesHeading", "Assistant notes")]
21063
+ }), /* @__PURE__ */ jsxs("span", {
21064
+ className: "text-muted-foreground",
21065
+ children: [tr("fluidgrids.agentMarks.notesOpenCount", "{{count}} open", { count: openCount }), orphaned > 0 ? ` · ${tr("fluidgrids.agentMarks.notesOrphanedCount", "{{count}} removed", { count: orphaned })}` : ""]
21066
+ })]
21067
+ }), expanded && /* @__PURE__ */ jsx("ul", {
21068
+ className: "max-h-64 overflow-y-auto border-t border-border py-1",
21069
+ children: notes.map((note) => {
21070
+ const onCanvas = live.has(note.nodeId);
21071
+ return /* @__PURE__ */ jsxs("li", {
21072
+ "data-testid": "fg-canvas-notes-item",
21073
+ className: "flex items-start gap-2 px-3 py-1.5",
21074
+ children: [
21075
+ /* @__PURE__ */ jsx("span", {
21076
+ className: cn(pinClass(note, false), "size-5 shrink-0"),
21077
+ children: note.resolved ? /* @__PURE__ */ jsx(Check, {
21078
+ "aria-hidden": "true",
21079
+ className: "size-3"
21080
+ }) : note.number
21081
+ }),
21082
+ /* @__PURE__ */ jsxs("button", {
21083
+ type: "button",
21084
+ disabled: !onCanvas,
21085
+ onClick: () => show(note),
21086
+ className: "min-w-0 flex-1 text-left disabled:cursor-default",
21087
+ children: [/* @__PURE__ */ jsx("span", {
21088
+ className: "block truncate font-medium",
21089
+ children: note.nodeLabel
21090
+ }), /* @__PURE__ */ jsx("span", {
21091
+ className: cn("block truncate text-muted-foreground", note.resolved && "line-through"),
21092
+ children: onCanvas ? note.message : tr("fluidgrids.agentMarks.noteOrphaned", "This node was removed from the workflow.")
21093
+ })]
21094
+ }),
21095
+ !onCanvas && /* @__PURE__ */ jsx("button", {
21096
+ type: "button",
21097
+ "aria-label": tr("fluidgrids.agentMarks.removeNote", "Remove note"),
21098
+ onClick: () => removeNote(note.id),
21099
+ className: "shrink-0 rounded p-0.5 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
21100
+ children: /* @__PURE__ */ jsx(X, {
21101
+ "aria-hidden": "true",
21102
+ className: "size-3.5"
21103
+ })
21104
+ })
21105
+ ]
21106
+ }, note.id);
21107
+ })
21108
+ })]
21109
+ })
21110
+ });
21111
+ };
21112
+ const CanvasNotesLayer = () => {
21113
+ const workflowKey = useFlowStore((state) => notesKeyOf(state.workflow));
21114
+ const nodeIds = useFlowStore(useShallow((state) => state.workflow.nodes.map((node) => node.id)));
21115
+ const allNotes = useAgentMarksStore((state) => state.notes);
21116
+ const notes = useMemo(() => allNotes.filter((note) => note.workflowKey === workflowKey), [allNotes, workflowKey]);
21117
+ const live = useMemo(() => new Set(nodeIds), [nodeIds]);
21118
+ const byNode = useMemo(() => {
21119
+ const groups = /* @__PURE__ */ new Map();
21120
+ for (const note of notes) {
21121
+ if (!live.has(note.nodeId)) continue;
21122
+ groups.set(note.nodeId, [...groups.get(note.nodeId) ?? [], note]);
21123
+ }
21124
+ return [...groups.entries()];
21125
+ }, [notes, live]);
21126
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
21127
+ /* @__PURE__ */ jsxs(ViewportPortal, { children: [byNode.map(([nodeId, nodeNotes]) => {
21128
+ const openNotes = nodeNotes.filter((note) => !note.resolved);
21129
+ if (openNotes.length === 0) return null;
21130
+ const tone = openNotes.some((note) => note.kind === "fix") ? "fix" : "info";
21131
+ return /* @__PURE__ */ jsx(NodeRing, {
21132
+ nodeId,
21133
+ tone
21134
+ }, nodeId);
21135
+ }), /* @__PURE__ */ jsx(FlashRing, {})] }),
21136
+ byNode.map(([nodeId, nodeNotes]) => /* @__PURE__ */ jsx(NodePins, {
21137
+ nodeId,
21138
+ notes: nodeNotes
21139
+ }, nodeId)),
21140
+ notes.length > 0 && /* @__PURE__ */ jsx(NotesList, {
21141
+ notes,
21142
+ live
21143
+ })
21144
+ ] });
21145
+ };
21146
+
20403
21147
  // src/builder/lib/autosave-runner.ts
20404
21148
  function createAutosaveRunner(deps) {
20405
21149
  let timer;
@@ -26841,8 +27585,8 @@ function EditLeaseBanner() {
26841
27585
  }
26842
27586
 
26843
27587
  // src/builder/components/flow-builder/flow-builder.tsx
26844
- const LazyNodeDetailView = lazy(() => import("./node-detail-view-Cfcpoa28.js").then((m) => ({ default: m.NodeDetailView })));
26845
- const LazyQuickConfigPanel = lazy(() => import("./quick-config-panel-DSEO6BGC.js").then((m) => ({ default: m.QuickConfigPanel })));
27588
+ const LazyNodeDetailView = lazy(() => import("./node-detail-view-CPD_NSZc.js").then((m) => ({ default: m.NodeDetailView })));
27589
+ const LazyQuickConfigPanel = lazy(() => import("./quick-config-panel-DHkYGaCL.js").then((m) => ({ default: m.QuickConfigPanel })));
26846
27590
  const edgeTypes = { deletable: CustomDeletableEdge };
26847
27591
  const edgeOptions = {
26848
27592
  type: "deletable",
@@ -26902,7 +27646,7 @@ const FlowBuilder = ({ readOnly = false }) => {
26902
27646
  const [agentTools, setAgentTools] = useState([]);
26903
27647
  useEffect(() => {
26904
27648
  let cancelled = false;
26905
- import("./agentTools-ZSPi5eH3.js").then(({ createBuilderAgentTools }) => {
27649
+ import("./agentTools-C0g2OWvI.js").then(({ createBuilderAgentTools }) => {
26906
27650
  if (cancelled) return;
26907
27651
  setAgentTools(createBuilderAgentTools({ durableFunctionsEnabled: () => paletteGatesRef.current }));
26908
27652
  }).catch((error) => {
@@ -26949,7 +27693,30 @@ const FlowBuilder = ({ readOnly = false }) => {
26949
27693
  }
26950
27694
  return merged;
26951
27695
  }, [nodeTypesVersion, nodes]);
26952
- const { getNodes } = useReactFlow();
27696
+ const { getNodes, fitView } = useReactFlow();
27697
+ const fitViewRef = useRef(fitView);
27698
+ useEffect(() => {
27699
+ fitViewRef.current = fitView;
27700
+ }, [fitView]);
27701
+ const [noteTools, setNoteTools] = useState([]);
27702
+ useEffect(() => {
27703
+ let cancelled = false;
27704
+ import("./canvasNoteTools-BUWY18XK.js").then(({ createCanvasNoteTools }) => {
27705
+ if (cancelled) return;
27706
+ setNoteTools(createCanvasNoteTools({ focusNode: (nodeId) => fitViewRef.current({
27707
+ nodes: [{ id: nodeId }],
27708
+ duration: 400,
27709
+ padding: .6,
27710
+ maxZoom: 1.25
27711
+ }) }));
27712
+ }).catch((error) => {
27713
+ console.error("[FlowBuilder] Assistant note tools failed to load:", error);
27714
+ });
27715
+ return () => {
27716
+ cancelled = true;
27717
+ };
27718
+ }, []);
27719
+ useAgentTools(noteTools);
26953
27720
  const { handleOnEdgeDropConnectEnd, floatingMenuWrapperRef, handleAddConnectedNode } = useAddNodeOnEdgeDrop();
26954
27721
  const deleteKeyCode = useDeleteKeyCode();
26955
27722
  useNodeSelectedEvent();
@@ -27019,7 +27786,7 @@ const FlowBuilder = ({ readOnly = false }) => {
27019
27786
  className: "relative flex-1 rounded-b-xl border border-card-foreground/10 border-t-0 overflow-hidden mx-4 mb-1",
27020
27787
  children: [
27021
27788
  /* @__PURE__ */ jsx(CanvasControls, {}),
27022
- /* @__PURE__ */ jsx(ReactFlow, {
27789
+ /* @__PURE__ */ jsxs(ReactFlow, {
27023
27790
  proOptions: { hideAttribution: true },
27024
27791
  onInit: ({ fitView }) => fitView({
27025
27792
  minZoom: 1,
@@ -27052,7 +27819,7 @@ const FlowBuilder = ({ readOnly = false }) => {
27052
27819
  maxZoom: 1
27053
27820
  },
27054
27821
  defaultEdgeOptions: edgeOptions,
27055
- children: /* @__PURE__ */ jsx(Background, { gap: 24 })
27822
+ children: [/* @__PURE__ */ jsx(Background, { gap: 24 }), /* @__PURE__ */ jsx(CanvasNotesLayer, {})]
27056
27823
  }),
27057
27824
  /* @__PURE__ */ jsx(FluidGridsMark, {}),
27058
27825
  nodes.length === 0 && !editingBlocked && /* @__PURE__ */ jsx("div", {
@@ -27242,4 +28009,4 @@ const FlowBuilderPage = ({ workflow, readOnly = false }) => {
27242
28009
  ] });
27243
28010
  };
27244
28011
 
27245
- export { CodeEditorField as $, DialogOverlay as A, NodeSettingsPanel as B, badgeVariants as C, isNodeTypeHiddenFromPalette as Ct, DialogDescription$1 as D, DialogContent$1 as E, useWorkflowTemplates as F, useRunnerActions as G, RunnerStatusPill as H, useInstallNode as I, useGlobalStoreClient as J, useRunners as K, useInstalledNodes as L, DialogTitle$1 as M, DialogTrigger$1 as N, DialogFooter$1 as O, computeAttachPosition as P, PopoverTrigger$1 as Q, NodeExecutionActions as R, Badge$1 as S, isDurableFunctionNodeType as St, DialogClose as T, RUNNER_INSTALL_SCRIPT_URL as U, RunOnSection as V, runnerInstallCommand as W, Popover$1 as X, NDV_Z_INDEX as Y, PopoverContent$1 as Z, TableView as _, Input$1 as _t, useAiWorkflowTemplates as a, SchedulePanel as at, TemplateCard as b, WORKFLOW_TEST_NODE_TYPES as bt, RunsUsageMeter as c, NodeInputFields as ct, readRunsQuota as d, Card$1 as dt, ConnectionSelector as et, runsQuotaLevel as f, CardContent$1 as ft, JsonView as g, CardTitle$1 as gt, useWorkflowReloadStore as h, CardHeader$1 as ht, enrichDraftNodes as i, SCHEDULE_NODE_TYPE as it, DialogPortal as j, DialogHeader$1 as k, RunsUsageMeterView as l, useNodeSelectedEvent as lt, releaseLeaseKeptForReload as m, CardFooter as mt, AI_DRAFT_MAX_PROMPT_LENGTH as n, ReadOnlyFieldset as nt, BUILDER_FULLSCREEN_ATTRIBUTE as o, NodeEditorTabs as ot, summarizeRunsQuota as p, CardDescription$1 as pt, NodeParamsForm as q, draftToCanvas as r, useNodeEditorTab as rt, BUILDER_FULLSCREEN_EVENT as s, TabButton as st, FlowBuilderPage as t, ConnectionFormDialog as tt, useRunsQuota as u, FluidGridsMark as ut, buildNodeCheck as v, DURABLE_FUNCTION_NODE_TYPES as vt, Dialog$1 as w, isRunControlNodeType as wt, templatePreview as x, filterPaletteNodes as xt, buildRequiredNodeTypes as y, RUN_CONTROL_NODE_TYPES as yt, NodeEditorTabBody as z };
28012
+ export { useGlobalStoreClient as $, DialogFooter$1 as A, useImportWorkflowTemplate as B, templatePreview as C, DURABLE_FUNCTION_NODE_TYPES as Ct, DialogClose as D, isDurableFunctionNodeType as Dt, Dialog$1 as E, filterPaletteNodes as Et, DialogTrigger$1 as F, NodeSettingsPanel as G, useInstalledNodes as H, computeAttachPosition as I, RUNNER_INSTALL_SCRIPT_URL as J, RunOnSection as K, useTemplateImportFlow as L, DialogOverlay as M, DialogPortal as N, DialogContent$1 as O, isNodeTypeHiddenFromPalette as Ot, DialogTitle$1 as P, NodeParamsForm as Q, buildNodeCheck as R, TemplateCard as S, Input$1 as St, badgeVariants as T, WORKFLOW_TEST_NODE_TYPES as Tt, NodeExecutionActions as U, useWorkflowTemplates as V, NodeEditorTabBody as W, useRunnerActions as X, runnerInstallCommand as Y, useRunners as Z, resetAgentMarks as _, CardContent$1 as _t, useAiWorkflowTemplates as a, ConnectionSelector as at, JsonView as b, CardHeader$1 as bt, RunsUsageMeter as c, useNodeEditorTab as ct, readRunsQuota as d, NodeEditorTabs as dt, NDV_Z_INDEX as et, runsQuotaLevel as f, TabButton as ft, notesKeyOf as g, Card$1 as gt, useWorkflowReloadStore as h, FluidGridsMark as ht, enrichDraftNodes as i, CodeEditorField as it, DialogHeader$1 as j, DialogDescription$1 as k, isRunControlNodeType as kt, RunsUsageMeterView as l, SCHEDULE_NODE_TYPE as lt, releaseLeaseKeptForReload as m, useNodeSelectedEvent as mt, AI_DRAFT_MAX_PROMPT_LENGTH as n, PopoverContent$1 as nt, BUILDER_FULLSCREEN_ATTRIBUTE as o, ConnectionFormDialog as ot, summarizeRunsQuota as p, NodeInputFields as pt, RunnerStatusPill as q, draftToCanvas as r, PopoverTrigger$1 as rt, BUILDER_FULLSCREEN_EVENT as s, ReadOnlyFieldset as st, FlowBuilderPage as t, Popover$1 as tt, useRunsQuota as u, SchedulePanel as ut, useAgentMarksStore as v, CardDescription$1 as vt, Badge$1 as w, RUN_CONTROL_NODE_TYPES as wt, TableView as x, CardTitle$1 as xt, toPlainText as y, CardFooter as yt, buildRequiredNodeTypes as z };