mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-10 08:05:22 +02:00
feat: add test mode for API trigger
This commit is contained in:
parent
f041e6030d
commit
4171ad7a54
13 changed files with 279 additions and 124 deletions
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"effortLevel": "high",
|
||||
"env": {
|
||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@ from api.mcp_server.instructions import DOGRAH_MCP_INSTRUCTIONS
|
|||
mcp = FastMCP("dograh", instructions=DOGRAH_MCP_INSTRUCTIONS)
|
||||
|
||||
from api.mcp_server.tools import catalog as _catalog # noqa: E402, F401
|
||||
from api.mcp_server.tools import get_workflow_code as _get_workflow_code # noqa: E402, F401
|
||||
from api.mcp_server.tools import (
|
||||
get_workflow_code as _get_workflow_code, # noqa: E402, F401
|
||||
)
|
||||
from api.mcp_server.tools import node_types as _node_types # noqa: E402, F401
|
||||
from api.mcp_server.tools import save_workflow as _save_workflow # noqa: E402, F401
|
||||
from api.mcp_server.tools import workflows as _workflows # noqa: E402, F401
|
||||
|
|
|
|||
|
|
@ -53,27 +53,17 @@ def trigger_exists_in_workflow(workflow_definition: dict, trigger_path: str) ->
|
|||
return False
|
||||
|
||||
|
||||
@router.post("/{uuid}", response_model=TriggerCallResponse)
|
||||
async def initiate_call(
|
||||
async def _initiate_call(
|
||||
uuid: str,
|
||||
request: TriggerCallRequest,
|
||||
x_api_key: str = Header(..., alias="X-API-Key"),
|
||||
):
|
||||
"""Initiate a phone call via API trigger.
|
||||
x_api_key: str,
|
||||
*,
|
||||
use_draft: bool,
|
||||
) -> TriggerCallResponse:
|
||||
"""Shared core for production and test trigger endpoints.
|
||||
|
||||
This endpoint allows external systems (CRMs, automation tools, etc.) to
|
||||
programmatically trigger outbound phone calls with custom context variables.
|
||||
|
||||
Args:
|
||||
uuid: The unique trigger UUID
|
||||
request: The call request with phone number and optional context
|
||||
x_api_key: API key for authentication (passed in X-API-Key header)
|
||||
|
||||
Returns:
|
||||
TriggerCallResponse with workflow run details
|
||||
|
||||
Raises:
|
||||
HTTPException: Various error conditions (401, 403, 404, 400)
|
||||
When ``use_draft`` is True the latest draft definition is executed;
|
||||
otherwise the published (released) definition is used.
|
||||
"""
|
||||
# 1. Validate API key
|
||||
api_key = await db_client.validate_api_key(x_api_key)
|
||||
|
|
@ -98,14 +88,23 @@ async def initiate_call(
|
|||
if not quota_result.has_quota:
|
||||
raise HTTPException(status_code=402, detail=quota_result.error_message)
|
||||
|
||||
# 5. Get workflow and validate trigger exists in definition
|
||||
# 5. Get workflow and resolve the definition (published vs draft)
|
||||
workflow = await db_client.get_workflow_by_id(trigger.workflow_id)
|
||||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
||||
|
||||
workflow_definition = workflow.released_definition.workflow_json
|
||||
if use_draft:
|
||||
draft = await db_client.get_draft_version(trigger.workflow_id)
|
||||
# Fall back to the published definition when no draft exists, so the
|
||||
# test URL always runs *something* — typically the same agent the
|
||||
# production URL would run.
|
||||
workflow_definition = (
|
||||
draft.workflow_json if draft else workflow.released_definition.workflow_json
|
||||
)
|
||||
else:
|
||||
workflow_definition = workflow.released_definition.workflow_json
|
||||
|
||||
# Validate trigger node still exists in the workflow definition
|
||||
# Validate trigger node still exists in the resolved definition
|
||||
if not trigger_exists_in_workflow(workflow_definition, uuid):
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
|
|
@ -126,7 +125,8 @@ async def initiate_call(
|
|||
workflow_run_mode = provider.PROVIDER_NAME
|
||||
|
||||
# 8. Create workflow run
|
||||
workflow_run_name = f"WR-API-{random.randint(1000, 9999)}"
|
||||
mode_label = "TEST" if use_draft else "API"
|
||||
workflow_run_name = f"WR-{mode_label}-{random.randint(1000, 9999)}"
|
||||
workflow_run = await db_client.create_workflow_run(
|
||||
name=workflow_run_name,
|
||||
workflow_id=trigger.workflow_id,
|
||||
|
|
@ -135,13 +135,16 @@ async def initiate_call(
|
|||
"provider": provider.PROVIDER_NAME,
|
||||
"phone_number": request.phone_number,
|
||||
"agent_uuid": uuid,
|
||||
"trigger_mode": "test" if use_draft else "production",
|
||||
**(request.initial_context or {}),
|
||||
},
|
||||
user_id=api_key.created_by,
|
||||
use_draft=use_draft,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Created workflow run {workflow_run.id} for API trigger {uuid} "
|
||||
f"(mode={'test' if use_draft else 'production'}) "
|
||||
f"to phone number {request.phone_number}"
|
||||
)
|
||||
|
||||
|
|
@ -183,3 +186,30 @@ async def initiate_call(
|
|||
workflow_run_id=workflow_run.id,
|
||||
workflow_run_name=workflow_run_name,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{uuid}", response_model=TriggerCallResponse)
|
||||
async def initiate_call(
|
||||
uuid: str,
|
||||
request: TriggerCallRequest,
|
||||
x_api_key: str = Header(..., alias="X-API-Key"),
|
||||
):
|
||||
"""Initiate a phone call against the published agent.
|
||||
|
||||
Executes the workflow's currently released definition.
|
||||
"""
|
||||
return await _initiate_call(uuid, request, x_api_key, use_draft=False)
|
||||
|
||||
|
||||
@router.post("/test/{uuid}", response_model=TriggerCallResponse)
|
||||
async def initiate_call_test(
|
||||
uuid: str,
|
||||
request: TriggerCallRequest,
|
||||
x_api_key: str = Header(..., alias="X-API-Key"),
|
||||
):
|
||||
"""Initiate a phone call against the latest draft of the agent.
|
||||
|
||||
Useful for verifying changes before publishing. Falls back to the
|
||||
published definition when no draft exists.
|
||||
"""
|
||||
return await _initiate_call(uuid, request, x_api_key, use_draft=True)
|
||||
|
|
|
|||
|
|
@ -56,7 +56,10 @@ async def get_user(
|
|||
# ------------------------------------------------------------------
|
||||
|
||||
try:
|
||||
user_model, user_was_created = await db_client.get_or_create_user_by_provider_id(stack_user["id"])
|
||||
(
|
||||
user_model,
|
||||
user_was_created,
|
||||
) = await db_client.get_or_create_user_by_provider_id(stack_user["id"])
|
||||
|
||||
# Sync email from Stack Auth if available and not already set
|
||||
stack_email = stack_user.get("primary_email_verified") and stack_user.get(
|
||||
|
|
|
|||
|
|
@ -660,7 +660,7 @@ TTSConfig = Annotated[
|
|||
###################################################### STT ########################################################################
|
||||
|
||||
|
||||
DEEPGRAM_STT_MODELS = ["nova-3-general", "flux-general-en"]
|
||||
DEEPGRAM_STT_MODELS = ["nova-3-general", "flux-general-en", "flux-general-multi"]
|
||||
DEEPGRAM_LANGUAGES = [
|
||||
"multi",
|
||||
"ar",
|
||||
|
|
|
|||
|
|
@ -13,11 +13,16 @@ from api.services.workflow.node_specs._base import (
|
|||
SPEC = NodeSpec(
|
||||
name="trigger",
|
||||
display_name="API Trigger",
|
||||
description="Public HTTP endpoint that launches the workflow.",
|
||||
description=("Public HTTP endpoints that launch the workflow."),
|
||||
llm_hint=(
|
||||
"Exposes a public HTTP POST endpoint. External systems call the URL "
|
||||
"(derived from the auto-generated `trigger_path`) to launch this "
|
||||
"workflow. Requires an API key in the `X-API-Key` header."
|
||||
"Exposes two public HTTP POST endpoints derived from the auto-generated "
|
||||
"`trigger_path`:\n"
|
||||
" • Production: `<backend>/api/v1/public/agent/<trigger_path>` — runs "
|
||||
"the published agent. Use this from production systems.\n"
|
||||
" • Test: `<backend>/api/v1/public/agent/test/<trigger_path>` — runs "
|
||||
"the latest draft, useful for verifying changes before publishing. "
|
||||
"Falls back to the published agent when no draft exists.\n"
|
||||
"Both require an API key in the `X-API-Key` header."
|
||||
),
|
||||
category=NodeCategory.trigger,
|
||||
icon="Webhook",
|
||||
|
|
@ -44,7 +49,12 @@ SPEC = NodeSpec(
|
|||
display_name="Trigger Path",
|
||||
description=(
|
||||
"Auto-generated UUID-style path segment that uniquely "
|
||||
"identifies this trigger. Do not edit manually."
|
||||
"identifies this trigger. Used in both URLs:\n"
|
||||
" • Production: `/api/v1/public/agent/<trigger_path>` — "
|
||||
"executes the published agent.\n"
|
||||
" • Test: `/api/v1/public/agent/test/<trigger_path>` — "
|
||||
"executes the latest draft.\n"
|
||||
"Do not edit manually."
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ Covers:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from dograh_sdk import Workflow
|
||||
from dograh_sdk._generated_models import NodeSpec
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ Covers:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from dograh_sdk import Workflow
|
||||
from dograh_sdk._generated_models import NodeSpec
|
||||
|
|
|
|||
|
|
@ -14,16 +14,26 @@ This is useful when you want to trigger calls from your own backend, a CRM, or w
|
|||
|
||||
## Finding your trigger URL
|
||||
|
||||
When you add an API Trigger node to your workflow, Dograh assigns it a unique UUID. You can find this UUID in the dashboard URL when viewing the agent, or in the trigger node settings.
|
||||
|
||||
Your trigger endpoint is:
|
||||
When you add an API Trigger node to your workflow, Dograh assigns it a unique UUID. The trigger node exposes two URLs that share this UUID — one for the published agent and one for the latest draft. You can copy either URL from the trigger node's settings dialog.
|
||||
|
||||
```
|
||||
POST https://your-dograh-instance/api/v1/public/agent/{uuid}
|
||||
POST https://your-dograh-instance/api/v1/public/agent/{uuid} # Production
|
||||
POST https://your-dograh-instance/api/v1/public/agent/test/{uuid} # Test
|
||||
```
|
||||
|
||||
If you are using the hosted version, replace `your-dograh-instance` with `api.dograh.com`.
|
||||
|
||||
### Test vs production
|
||||
|
||||
| Mode | URL | Runs |
|
||||
|------------|------------------------------------|---------------------------------------------------------------------------|
|
||||
| Production | `/api/v1/public/agent/{uuid}` | The published version of the agent. |
|
||||
| Test | `/api/v1/public/agent/test/{uuid}` | The latest draft. Falls back to the published version if no draft exists. |
|
||||
|
||||
Use the test URL while iterating on changes so production traffic continues to hit the published version. Once you publish your draft, both URLs run the same definition.
|
||||
|
||||
The request body, headers, and response shape are identical for both URLs.
|
||||
|
||||
## Making a request
|
||||
|
||||
Authenticate by passing your API key in the `X-API-Key` header. The request body requires a `phone_number` and accepts an optional `initial_context` object.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -2110,30 +2110,6 @@ export type MpsCreditsResponse = {
|
|||
total_quota: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* MigrationSpec
|
||||
*
|
||||
* Declared migration step (JSON-serializable view).
|
||||
*
|
||||
* The migrate callable is registered out-of-band via `register_migration()`
|
||||
* and never serialized — LLM and frontend consumers only see version
|
||||
* metadata and warn on mismatch.
|
||||
*/
|
||||
export type MigrationSpec = {
|
||||
/**
|
||||
* From Version
|
||||
*/
|
||||
from_version: string;
|
||||
/**
|
||||
* To Version
|
||||
*/
|
||||
to_version: string;
|
||||
/**
|
||||
* Description
|
||||
*/
|
||||
description: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* NodeCategory
|
||||
*
|
||||
|
|
@ -2206,10 +2182,6 @@ export type NodeSpec = {
|
|||
* Examples
|
||||
*/
|
||||
examples?: Array<NodeExample>;
|
||||
/**
|
||||
* Migrations
|
||||
*/
|
||||
migrations?: Array<MigrationSpec>;
|
||||
graph_constraints?: GraphConstraints | null;
|
||||
};
|
||||
|
||||
|
|
@ -8875,6 +8847,46 @@ export type InitiateCallApiV1PublicAgentUuidPostResponses = {
|
|||
|
||||
export type InitiateCallApiV1PublicAgentUuidPostResponse = InitiateCallApiV1PublicAgentUuidPostResponses[keyof InitiateCallApiV1PublicAgentUuidPostResponses];
|
||||
|
||||
export type InitiateCallTestApiV1PublicAgentTestUuidPostData = {
|
||||
body: TriggerCallRequest;
|
||||
headers: {
|
||||
/**
|
||||
* X-Api-Key
|
||||
*/
|
||||
'X-API-Key': string;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Uuid
|
||||
*/
|
||||
uuid: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/public/agent/test/{uuid}';
|
||||
};
|
||||
|
||||
export type InitiateCallTestApiV1PublicAgentTestUuidPostErrors = {
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type InitiateCallTestApiV1PublicAgentTestUuidPostError = InitiateCallTestApiV1PublicAgentTestUuidPostErrors[keyof InitiateCallTestApiV1PublicAgentTestUuidPostErrors];
|
||||
|
||||
export type InitiateCallTestApiV1PublicAgentTestUuidPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: TriggerCallResponse;
|
||||
};
|
||||
|
||||
export type InitiateCallTestApiV1PublicAgentTestUuidPostResponse = InitiateCallTestApiV1PublicAgentTestUuidPostResponses[keyof InitiateCallTestApiV1PublicAgentTestUuidPostResponses];
|
||||
|
||||
export type DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData = {
|
||||
body?: never;
|
||||
path: {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ import { NodeEditForm, useNodeSpecs } from "@/components/flow/renderer";
|
|||
import { ToolBadges } from "@/components/flow/ToolBadges";
|
||||
import { FlowNodeData } from "@/components/flow/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { NODE_DOCUMENTATION_URLS } from "@/constants/documentation";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { NodeContent } from "./common/NodeContent";
|
||||
import { NodeEditDialog } from "./common/NodeEditDialog";
|
||||
|
|
@ -80,12 +82,22 @@ function seedValues(
|
|||
return out;
|
||||
}
|
||||
|
||||
function buildTriggerEndpoint(triggerPath: string | undefined): string {
|
||||
if (!triggerPath) return "";
|
||||
interface TriggerEndpoints {
|
||||
production: string;
|
||||
test: string;
|
||||
}
|
||||
|
||||
function buildTriggerEndpoints(
|
||||
triggerPath: string | undefined,
|
||||
): TriggerEndpoints {
|
||||
if (!triggerPath) return { production: "", test: "" };
|
||||
const backendUrl =
|
||||
process.env.NEXT_PUBLIC_BACKEND_URL ||
|
||||
(typeof window !== "undefined" ? window.location.origin : "");
|
||||
return `${backendUrl}/api/v1/public/agent/${triggerPath}`;
|
||||
return {
|
||||
production: `${backendUrl}/api/v1/public/agent/${triggerPath}`,
|
||||
test: `${backendUrl}/api/v1/public/agent/test/${triggerPath}`,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Canvas preview dispatch ──────────────────────────────────────────────
|
||||
|
|
@ -106,7 +118,7 @@ function CanvasPreview({
|
|||
onStaleDocuments: (uuids: string[]) => void;
|
||||
}) {
|
||||
if (spec.name === "trigger") {
|
||||
const endpoint = buildTriggerEndpoint(data.trigger_path);
|
||||
const endpoint = buildTriggerEndpoints(data.trigger_path).production;
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground">API Endpoint:</p>
|
||||
|
|
@ -229,21 +241,105 @@ function StatusDot({ enabled }: { enabled: boolean }) {
|
|||
);
|
||||
}
|
||||
|
||||
// ─── Trigger curl example helper (rendered inside the dialog form) ────────
|
||||
// ─── Trigger webhook URLs (test + production) — rendered inside the dialog ─
|
||||
|
||||
function TriggerCurlExample({ endpoint }: { endpoint: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const curl = `curl -X POST "${endpoint}" \\
|
||||
function buildCurl(endpoint: string): string {
|
||||
return `curl -X POST "${endpoint}" \\
|
||||
-H "X-API-Key: YOUR_API_KEY" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"phone_number": "+1234567890", "initial_context": {}}'`;
|
||||
}
|
||||
|
||||
function ClickToCopy({
|
||||
value,
|
||||
children,
|
||||
className,
|
||||
title,
|
||||
}: {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const onCopy = async () => {
|
||||
if (!value) return;
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCopy}
|
||||
title={title ?? "Click to copy"}
|
||||
className={cn(
|
||||
"group relative text-left transition-colors hover:bg-accent/60 cursor-pointer disabled:cursor-default",
|
||||
className,
|
||||
)}
|
||||
disabled={!value}
|
||||
>
|
||||
{children}
|
||||
<span
|
||||
aria-hidden={!copied}
|
||||
className={cn(
|
||||
"pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded bg-foreground/90 px-1.5 py-0.5 text-[10px] font-medium text-background shadow transition-opacity",
|
||||
copied ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
Copied!
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function UrlPanel({
|
||||
endpoint,
|
||||
helperText,
|
||||
}: {
|
||||
endpoint: string;
|
||||
helperText: string;
|
||||
}) {
|
||||
const curl = endpoint ? buildCurl(endpoint) : "";
|
||||
return (
|
||||
<div className="grid gap-2 pt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded shrink-0">
|
||||
POST
|
||||
</span>
|
||||
<ClickToCopy
|
||||
value={endpoint}
|
||||
title="Click to copy URL"
|
||||
className="flex-1 bg-muted rounded px-2 py-1"
|
||||
>
|
||||
<code className="text-xs break-all">
|
||||
{endpoint || "Generating..."}
|
||||
</code>
|
||||
</ClickToCopy>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{helperText}</p>
|
||||
<p className="text-sm font-medium pt-2">Example Request</p>
|
||||
<ClickToCopy
|
||||
value={curl}
|
||||
title="Click to copy curl"
|
||||
className="block w-full bg-muted rounded"
|
||||
>
|
||||
<pre className="text-xs px-3 py-2 overflow-x-auto whitespace-pre-wrap">
|
||||
{curl || "Generating..."}
|
||||
</pre>
|
||||
</ClickToCopy>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TriggerWebhookUrls({ endpoints }: { endpoints: TriggerEndpoints }) {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm font-medium">API Endpoint</p>
|
||||
<p className="text-sm font-medium">Webhook URLs</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Use this endpoint to trigger calls via API. Requires an API key in
|
||||
the X-API-Key header.{" "}
|
||||
Test mode runs the latest draft so you can verify changes before
|
||||
publishing. Production runs the published agent. Both require an
|
||||
API key in the X-API-Key header.{" "}
|
||||
<Link
|
||||
href="/api-keys"
|
||||
target="_blank"
|
||||
|
|
@ -252,27 +348,24 @@ function TriggerCurlExample({ endpoint }: { endpoint: string }) {
|
|||
Get your API key
|
||||
</Link>
|
||||
</p>
|
||||
<code className="text-xs break-all bg-muted px-2 py-1 rounded">
|
||||
{endpoint || "Generating..."}
|
||||
</code>
|
||||
<p className="text-sm font-medium pt-2">Example Request</p>
|
||||
<div className="relative">
|
||||
<pre className="text-xs bg-muted px-3 py-2 rounded overflow-x-auto whitespace-pre-wrap">
|
||||
{curl}
|
||||
</pre>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2"
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(curl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}}
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
<Tabs defaultValue="test" className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="test">Test URL</TabsTrigger>
|
||||
<TabsTrigger value="production">Production URL</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="test">
|
||||
<UrlPanel
|
||||
endpoint={endpoints.test}
|
||||
helperText="Runs the latest draft, falling back to the published agent when no draft exists."
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="production">
|
||||
<UrlPanel
|
||||
endpoint={endpoints.production}
|
||||
helperText="Runs the published agent."
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -318,7 +411,7 @@ export const GenericNode = memo(({ data, selected, id, type }: GenericNodeProps)
|
|||
// ── Trigger auto-UUID + canvas copy state ──────────────────────────
|
||||
const [triggerCopied, setTriggerCopied] = useState(false);
|
||||
const handleCopyTrigger = useCallback(async () => {
|
||||
const endpoint = buildTriggerEndpoint(data.trigger_path);
|
||||
const endpoint = buildTriggerEndpoints(data.trigger_path).production;
|
||||
if (!endpoint) return;
|
||||
await navigator.clipboard.writeText(endpoint);
|
||||
setTriggerCopied(true);
|
||||
|
|
@ -472,8 +565,8 @@ export const GenericNode = memo(({ data, selected, id, type }: GenericNodeProps)
|
|||
}}
|
||||
/>
|
||||
{type === "trigger" && (
|
||||
<TriggerCurlExample
|
||||
endpoint={buildTriggerEndpoint(data.trigger_path)}
|
||||
<TriggerWebhookUrls
|
||||
endpoints={buildTriggerEndpoints(data.trigger_path)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue