@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.
- package/dist/{ExecutionDetailPage-DFjVF9B6.js → ExecutionDetailPage-CDkJeeu2.js} +9 -10
- package/dist/{FailureTriagePanel-CR1BzOrM.js → FailureTriagePanel-BHiduL7A.js} +1 -1
- package/dist/{FluidGridsProvider-DpTTWDNQ.js → FluidGridsProvider-CDZjKmfD.js} +1 -2
- package/dist/{HistoryPage-CfefCOUa.js → HistoryPage-DnVGN_72.js} +3 -3
- package/dist/{InsightsPage-Cbf0cdbC.js → InsightsPage-CGRpZtGu.js} +4 -4
- package/dist/{ManagedAiPolicyAuthority-BO67oJlr.js → ManagedAiPolicyAuthority-DbJBledZ.js} +6 -6
- package/dist/{MissingNodesInstallModal-DuOtLV6V.js → MissingNodesInstallModal-Ca4f--lw.js} +2 -2
- package/dist/{NodesPage-C4KtAasX.js → NodesPage-ClvhUfbh.js} +2 -2
- package/dist/{PublishCustomNodeAction-C3fEmWnC.js → PublishCustomNodeAction-D2F7oN0S.js} +1 -1
- package/dist/{TemplateConfigurationModal-DwJi6alN.js → TemplateConfigurationModal-ChQsXf0-.js} +2 -2
- package/dist/{TemplateDetailPanel-Cpzjif1n.js → TemplateDetailPanel-C0YiVdub.js} +2 -2
- package/dist/{TemplatesPage-B3fjphKN.js → TemplatesPage-QXtq6mI3.js} +113 -504
- package/dist/{VersionDiffDialog-ExN-CN6f.js → VersionDiffDialog-Dq2xlPFI.js} +1 -1
- package/dist/{WorkflowProposalCard-DuXQI7UY.js → WorkflowProposalCard-8Vr56kQa.js} +1 -1
- package/dist/{WorkflowSearchRedirect-D4JvOvoT.js → WorkflowSearchRedirect-B4nHALJg.js} +10 -11
- package/dist/{WorkflowShareDialog-ByCefHCP.js → WorkflowShareDialog-ByTSWwwR.js} +2 -2
- package/dist/{WorkflowsList-XPY9rJpX.js → WorkflowsList-B9F2e8M6.js} +5 -6
- package/dist/{agent-form-CzG3vX_q.js → agent-form-BvMWhQ42.js} +1 -1
- package/dist/{agentTools-ZSPi5eH3.js → agentTools-C0g2OWvI.js} +2 -2
- package/dist/canvasNoteTools-BUWY18XK.js +136 -0
- package/dist/{codec-client-DiFkc0yn.js → codec-client-DstqyzTN.js} +1 -1
- package/dist/executions.js +1 -1
- package/dist/{flow-builder-BjF8QGp5.js → flow-builder-BvCQAGDR.js} +1552 -785
- package/dist/history.js +1 -1
- package/dist/{index-BES5GtJJ.d.ts → index-CLqh2u28.d.ts} +73 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +447 -26
- package/dist/{node-detail-view-Cfcpoa28.js → node-detail-view-CPD_NSZc.js} +6 -6
- package/dist/{node-endpoint-configs-DVo9nUOe.js → node-endpoint-configs-DHFGA5MQ.js} +17 -3
- package/dist/nodes.js +1 -1
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +1 -1
- package/dist/{quick-config-panel-DSEO6BGC.js → quick-config-panel-DHkYGaCL.js} +2 -2
- package/dist/settings.js +1 -1
- package/dist/templates.js +1 -1
- package/dist/{useAgentDefinitions-BVTONKpB.js → useAgentDefinitions-NFtLytDR.js} +1 -1
- package/dist/{useExecutions-C84_O9Qv.js → useExecutions-BqoD7XWi.js} +2 -2
- package/dist/{useObservability--XT0i9Lc.js → useObservability-Cs-5_NHh.js} +2 -2
- package/dist/{useRunHistory-B0IcKx_P.js → useRunHistory-CEL-CIz-.js} +2 -2
- package/dist/{useWorkflows-CDNmiA8o.js → useWorkflows-Bwpvfdgv.js} +2 -2
- package/dist/workflow-builder.js +2 -2
- package/dist/workflows-list.js +1 -1
- package/package.json +1 -1
- package/dist/rolldown-runtime-hDbaFkMy.js +0 -18
- 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-
|
|
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 {
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
16
|
-
import { n as VersionLifecycleBar, r as countLifecycle, t as VersionDiffDialog } from "./VersionDiffDialog-
|
|
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$
|
|
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$
|
|
3646
|
+
const source = isRecord$6(state) ? state : {};
|
|
3647
3647
|
let rules;
|
|
3648
3648
|
if (Array.isArray(source.rules)) {
|
|
3649
|
-
rules = source.rules.filter(isRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
4350
|
-
const state = isRecord$
|
|
4351
|
-
const name = isRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
4948
|
-
const recordTypes = Array.isArray(item.recordTypes) ? item.recordTypes.flatMap((type) => isRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
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/
|
|
9917
|
-
|
|
9918
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
9925
|
-
*
|
|
9926
|
-
*
|
|
9927
|
-
*
|
|
9928
|
-
*
|
|
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
|
|
9968
|
+
function useTemplateDetail() {
|
|
9931
9969
|
const client = useGlobalStoreClient();
|
|
9932
|
-
const
|
|
9933
|
-
const
|
|
9934
|
-
const
|
|
9935
|
-
const
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
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
|
-
|
|
9999
|
-
|
|
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/
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
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/
|
|
10106
|
-
|
|
10107
|
-
|
|
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
|
|
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-
|
|
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 {
|
|
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
|
-
|
|
12452
|
-
|
|
12453
|
-
|
|
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
|
-
|
|
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-
|
|
26845
|
-
const LazyQuickConfigPanel = lazy(() => import("./quick-config-panel-
|
|
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-
|
|
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__ */
|
|
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 {
|
|
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 };
|