Improve workflow run loop/conditional timeline UX follow-up (#SKY-7367) (#4782)
Co-authored-by: Suchintan Singh <suchintan@skyvern.com>
This commit is contained in:
@@ -49,38 +49,46 @@ const artifactApiClient = axios.create({
|
|||||||
baseURL: artifactApiBaseUrl,
|
baseURL: artifactApiBaseUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const clients = [client, v2Client, clientSansApiV1] as const;
|
||||||
|
|
||||||
|
function setHeaderForAllClients(header: string, value: string) {
|
||||||
|
clients.forEach((instance) => {
|
||||||
|
instance.defaults.headers.common[header] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeHeaderForAllClients(header: string) {
|
||||||
|
clients.forEach((instance) => {
|
||||||
|
// Axios stores headers at both `common` (shared across methods) and the
|
||||||
|
// top-level defaults object (can be set by initial config spread). Delete
|
||||||
|
// from both locations to ensure the header is fully removed.
|
||||||
|
delete instance.defaults.headers.common[header];
|
||||||
|
delete (instance.defaults.headers as Record<string, unknown>)[header];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function setAuthorizationHeader(token: string) {
|
export function setAuthorizationHeader(token: string) {
|
||||||
client.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
setHeaderForAllClients("Authorization", `Bearer ${token}`);
|
||||||
v2Client.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
||||||
clientSansApiV1.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeAuthorizationHeader() {
|
export function removeAuthorizationHeader() {
|
||||||
if (client.defaults.headers.common["Authorization"]) {
|
removeHeaderForAllClients("Authorization");
|
||||||
delete client.defaults.headers.common["Authorization"];
|
|
||||||
delete v2Client.defaults.headers.common["Authorization"];
|
|
||||||
delete clientSansApiV1.defaults.headers.common["Authorization"];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setApiKeyHeader(apiKey: string) {
|
export function setApiKeyHeader(apiKey: string) {
|
||||||
persistRuntimeApiKey(apiKey);
|
persistRuntimeApiKey(apiKey);
|
||||||
client.defaults.headers.common["X-API-Key"] = apiKey;
|
setHeaderForAllClients("X-API-Key", apiKey);
|
||||||
v2Client.defaults.headers.common["X-API-Key"] = apiKey;
|
|
||||||
clientSansApiV1.defaults.headers.common["X-API-Key"] = apiKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeApiKeyHeader() {
|
export function removeApiKeyHeader({
|
||||||
clearRuntimeApiKey();
|
clearStoredKey = true,
|
||||||
if (client.defaults.headers.common["X-API-Key"]) {
|
}: {
|
||||||
delete client.defaults.headers.common["X-API-Key"];
|
clearStoredKey?: boolean;
|
||||||
}
|
} = {}) {
|
||||||
if (v2Client.defaults.headers.common["X-API-Key"]) {
|
if (clearStoredKey) {
|
||||||
delete v2Client.defaults.headers.common["X-API-Key"];
|
clearRuntimeApiKey();
|
||||||
}
|
|
||||||
if (clientSansApiV1.defaults.headers.common["X-API-Key"]) {
|
|
||||||
delete clientSansApiV1.defaults.headers.common["X-API-Key"];
|
|
||||||
}
|
}
|
||||||
|
removeHeaderForAllClients("X-API-Key");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getClient(
|
async function getClient(
|
||||||
@@ -101,17 +109,24 @@ async function getClient(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (credentialGetter) {
|
// Both auth headers are sent intentionally: Authorization (Bearer token from
|
||||||
removeApiKeyHeader();
|
// the credential getter, e.g. Clerk) is used for user-session auth, while
|
||||||
|
// X-API-Key is used for org-level API key auth. The backend accepts either
|
||||||
const credential = await credentialGetter();
|
// and gives precedence to the API key when both are present. Sending both
|
||||||
|
// ensures requests succeed regardless of which auth method the org uses.
|
||||||
if (!credential) {
|
const credential = credentialGetter ? await credentialGetter() : null;
|
||||||
console.warn("No credential found");
|
if (credential) {
|
||||||
return get();
|
|
||||||
}
|
|
||||||
|
|
||||||
setAuthorizationHeader(credential);
|
setAuthorizationHeader(credential);
|
||||||
|
} else {
|
||||||
|
removeAuthorizationHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = getRuntimeApiKey();
|
||||||
|
if (apiKey) {
|
||||||
|
setHeaderForAllClients("X-API-Key", apiKey);
|
||||||
|
} else {
|
||||||
|
// Avoid mutating persisted keys here - just control request headers.
|
||||||
|
removeApiKeyHeader({ clearStoredKey: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
return get();
|
return get();
|
||||||
|
|||||||
@@ -114,11 +114,11 @@ export async function getSseClient(
|
|||||||
|
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
requestHeaders.Authorization = `Bearer ${authToken}`;
|
requestHeaders.Authorization = `Bearer ${authToken}`;
|
||||||
} else {
|
}
|
||||||
const apiKey = getRuntimeApiKey();
|
|
||||||
if (apiKey) {
|
const apiKey = getRuntimeApiKey();
|
||||||
requestHeaders["X-API-Key"] = apiKey;
|
if (apiKey) {
|
||||||
}
|
requestHeaders["X-API-Key"] = apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { statusIsFinalized } from "@/routes/tasks/types";
|
import { statusIsFinalized } from "@/routes/tasks/types";
|
||||||
|
import { cn } from "@/util/utils";
|
||||||
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
|
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
|
||||||
import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery";
|
import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery";
|
||||||
import {
|
import {
|
||||||
isBlockItem,
|
isBlockItem,
|
||||||
isObserverThought,
|
isObserverThought,
|
||||||
isTaskVariantBlockItem,
|
|
||||||
isThoughtItem,
|
isThoughtItem,
|
||||||
ObserverThought,
|
ObserverThought,
|
||||||
WorkflowRunBlock,
|
WorkflowRunBlock,
|
||||||
} from "../types/workflowRunTypes";
|
} from "../types/workflowRunTypes";
|
||||||
|
import { countActions } from "../utils";
|
||||||
import { ThoughtCard } from "@/routes/workflows/workflowRun/ThoughtCard";
|
import { ThoughtCard } from "@/routes/workflows/workflowRun/ThoughtCard";
|
||||||
import {
|
import {
|
||||||
ActionItem,
|
ActionItem,
|
||||||
WorkflowRunOverviewActiveElement,
|
WorkflowRunOverviewActiveElement,
|
||||||
} from "@/routes/workflows/workflowRun/WorkflowRunOverview";
|
} from "@/routes/workflows/workflowRun/WorkflowRunOverview";
|
||||||
import { WorkflowRunTimelineBlockItem } from "@/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem";
|
import { WorkflowRunTimelineBlockItem } from "@/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem";
|
||||||
import { cn } from "@/util/utils";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activeItem: WorkflowRunOverviewActiveElement;
|
activeItem: WorkflowRunOverviewActiveElement;
|
||||||
@@ -48,12 +48,7 @@ function DebuggerRunTimeline({
|
|||||||
|
|
||||||
const workflowRunIsFinalized = statusIsFinalized(workflowRun);
|
const workflowRunIsFinalized = statusIsFinalized(workflowRun);
|
||||||
|
|
||||||
const numberOfActions = workflowRunTimeline.reduce((total, current) => {
|
const numberOfActions = countActions(workflowRunTimeline);
|
||||||
if (isTaskVariantBlockItem(current)) {
|
|
||||||
return total + current.block!.actions!.length;
|
|
||||||
}
|
|
||||||
return total + 0;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const firstActionOrThoughtIsPending =
|
const firstActionOrThoughtIsPending =
|
||||||
!workflowRunIsFinalized && workflowRunTimeline.length === 0;
|
!workflowRunIsFinalized && workflowRunTimeline.length === 0;
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import type { WorkflowParameter } from "./types/workflowTypes";
|
import type { WorkflowParameter } from "./types/workflowTypes";
|
||||||
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
|
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
|
||||||
|
import {
|
||||||
|
isBlockItem,
|
||||||
|
isTaskVariantBlockItem,
|
||||||
|
WorkflowRunTimelineItem,
|
||||||
|
} from "./types/workflowRunTypes";
|
||||||
|
|
||||||
type Location = ReturnType<typeof useLocation>;
|
type Location = ReturnType<typeof useLocation>;
|
||||||
|
|
||||||
@@ -73,6 +78,20 @@ export const formatDuration = (duration: Duration): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function countActions(items: Array<WorkflowRunTimelineItem>): number {
|
||||||
|
return items.reduce((total, item) => {
|
||||||
|
if (!isBlockItem(item)) {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockActionCount = isTaskVariantBlockItem(item)
|
||||||
|
? item.block.actions?.length ?? 0
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return total + blockActionCount + countActions(item.children);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
export const getOrderedBlockLabels = (workflow?: WorkflowApiResponse) => {
|
export const getOrderedBlockLabels = (workflow?: WorkflowApiResponse) => {
|
||||||
if (!workflow) {
|
if (!workflow) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuer
|
|||||||
import {
|
import {
|
||||||
isBlockItem,
|
isBlockItem,
|
||||||
isObserverThought,
|
isObserverThought,
|
||||||
isTaskVariantBlockItem,
|
|
||||||
isThoughtItem,
|
isThoughtItem,
|
||||||
ObserverThought,
|
ObserverThought,
|
||||||
WorkflowRunBlock,
|
WorkflowRunBlock,
|
||||||
} from "../types/workflowRunTypes";
|
} from "../types/workflowRunTypes";
|
||||||
|
import { countActions } from "../utils";
|
||||||
import { ThoughtCard } from "./ThoughtCard";
|
import { ThoughtCard } from "./ThoughtCard";
|
||||||
import {
|
import {
|
||||||
ActionItem,
|
ActionItem,
|
||||||
@@ -56,12 +56,7 @@ function WorkflowRunTimeline({
|
|||||||
const finallyBlockLabel =
|
const finallyBlockLabel =
|
||||||
workflowRun.workflow?.workflow_definition?.finally_block_label ?? null;
|
workflowRun.workflow?.workflow_definition?.finally_block_label ?? null;
|
||||||
|
|
||||||
const numberOfActions = workflowRunTimeline.reduce((total, current) => {
|
const numberOfActions = countActions(workflowRunTimeline);
|
||||||
if (isTaskVariantBlockItem(current)) {
|
|
||||||
return total + current.block!.actions!.length;
|
|
||||||
}
|
|
||||||
return total + 0;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-w-0 space-y-4 rounded bg-slate-elevation1 p-4">
|
<div className="min-w-0 space-y-4 rounded bg-slate-elevation1 p-4">
|
||||||
|
|||||||
@@ -1,51 +1,190 @@
|
|||||||
import {
|
import {
|
||||||
CheckCircledIcon,
|
CheckCircledIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
CrossCircledIcon,
|
CrossCircledIcon,
|
||||||
CubeIcon,
|
CubeIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { useCallback } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Status } from "@/api/types";
|
import { Status } from "@/api/types";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
import { formatDuration, toDuration } from "@/routes/workflows/utils";
|
import { formatDuration, toDuration } from "@/routes/workflows/utils";
|
||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
import { workflowBlockTitle } from "../editor/nodes/types";
|
import { workflowBlockTitle } from "../editor/nodes/types";
|
||||||
import { WorkflowBlockIcon } from "../editor/nodes/WorkflowBlockIcon";
|
import { WorkflowBlockIcon } from "../editor/nodes/WorkflowBlockIcon";
|
||||||
import {
|
import {
|
||||||
|
hasEvaluations,
|
||||||
isAction,
|
isAction,
|
||||||
isBlockItem,
|
isBlockItem,
|
||||||
isObserverThought,
|
isObserverThought,
|
||||||
isThoughtItem,
|
isThoughtItem,
|
||||||
isWorkflowRunBlock,
|
isWorkflowRunBlock,
|
||||||
hasEvaluations,
|
ObserverThought,
|
||||||
WorkflowRunBlock,
|
WorkflowRunBlock,
|
||||||
WorkflowRunTimelineItem,
|
WorkflowRunTimelineItem,
|
||||||
} from "../types/workflowRunTypes";
|
} from "../types/workflowRunTypes";
|
||||||
|
import { isTaskVariantBlock } from "../types/workflowTypes";
|
||||||
import { ActionCard } from "./ActionCard";
|
import { ActionCard } from "./ActionCard";
|
||||||
|
import { WorkflowRunHumanInteraction } from "./WorkflowRunHumanInteraction";
|
||||||
import {
|
import {
|
||||||
ActionItem,
|
ActionItem,
|
||||||
WorkflowRunOverviewActiveElement,
|
WorkflowRunOverviewActiveElement,
|
||||||
} from "./WorkflowRunOverview";
|
} from "./WorkflowRunOverview";
|
||||||
import { ThoughtCard } from "./ThoughtCard";
|
import { ThoughtCard } from "./ThoughtCard";
|
||||||
import { ObserverThought } from "../types/workflowRunTypes";
|
|
||||||
import { isTaskVariantBlock } from "../types/workflowTypes";
|
|
||||||
import { WorkflowRunHumanInteraction } from "./WorkflowRunHumanInteraction";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activeItem: WorkflowRunOverviewActiveElement;
|
activeItem: WorkflowRunOverviewActiveElement;
|
||||||
block: WorkflowRunBlock;
|
block: WorkflowRunBlock;
|
||||||
subItems: Array<WorkflowRunTimelineItem>;
|
subItems: Array<WorkflowRunTimelineItem>;
|
||||||
|
depth?: number;
|
||||||
onBlockItemClick: (block: WorkflowRunBlock) => void;
|
onBlockItemClick: (block: WorkflowRunBlock) => void;
|
||||||
onActionClick: (action: ActionItem) => void;
|
onActionClick: (action: ActionItem) => void;
|
||||||
onThoughtCardClick: (thought: ObserverThought) => void;
|
onThoughtCardClick: (thought: ObserverThought) => void;
|
||||||
finallyBlockLabel?: string | null;
|
finallyBlockLabel?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type LoopIterationGroup = {
|
||||||
|
index: number | null;
|
||||||
|
currentValue: string | null;
|
||||||
|
items: Array<WorkflowRunTimelineItem>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function stringifyTimelineValue(value: unknown): string {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof value === "string" ||
|
||||||
|
typeof value === "number" ||
|
||||||
|
typeof value === "boolean"
|
||||||
|
) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateValue(value: string, maxLength = 120): string {
|
||||||
|
const collapsed = value.replace(/\s+/g, " ").trim();
|
||||||
|
if (collapsed.length <= maxLength) {
|
||||||
|
return collapsed;
|
||||||
|
}
|
||||||
|
return `${collapsed.slice(0, maxLength - 3)}...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLoopIterationGroups(
|
||||||
|
items: Array<WorkflowRunTimelineItem>,
|
||||||
|
): Array<LoopIterationGroup> {
|
||||||
|
const groupsByKey = new Map<string, LoopIterationGroup>();
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const currentIndex = isBlockItem(item) ? item.block.current_index : null;
|
||||||
|
const currentValue = isBlockItem(item) ? item.block.current_value : null;
|
||||||
|
const groupKey =
|
||||||
|
currentIndex === null ? "unknown" : `index-${currentIndex}`;
|
||||||
|
|
||||||
|
if (!groupsByKey.has(groupKey)) {
|
||||||
|
groupsByKey.set(groupKey, {
|
||||||
|
index: currentIndex,
|
||||||
|
currentValue,
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = groupsByKey.get(groupKey)!;
|
||||||
|
if (!group.currentValue && currentValue) {
|
||||||
|
group.currentValue = currentValue;
|
||||||
|
}
|
||||||
|
group.items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(groupsByKey.values()).sort((left, right) => {
|
||||||
|
if (left.index === null && right.index === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (left.index === null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (right.index === null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return right.index - left.index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimelineSubItemsProps = {
|
||||||
|
items: Array<WorkflowRunTimelineItem>;
|
||||||
|
activeItem: WorkflowRunOverviewActiveElement;
|
||||||
|
depth: number;
|
||||||
|
onBlockItemClick: (block: WorkflowRunBlock) => void;
|
||||||
|
onActionClick: (action: ActionItem) => void;
|
||||||
|
onThoughtCardClick: (thought: ObserverThought) => void;
|
||||||
|
finallyBlockLabel?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function TimelineSubItems({
|
||||||
|
items,
|
||||||
|
activeItem,
|
||||||
|
depth,
|
||||||
|
onBlockItemClick,
|
||||||
|
onActionClick,
|
||||||
|
onThoughtCardClick,
|
||||||
|
finallyBlockLabel,
|
||||||
|
}: TimelineSubItemsProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{items.map((item) => {
|
||||||
|
if (isBlockItem(item)) {
|
||||||
|
return (
|
||||||
|
<WorkflowRunTimelineBlockItem
|
||||||
|
key={item.block.workflow_run_block_id}
|
||||||
|
subItems={item.children}
|
||||||
|
activeItem={activeItem}
|
||||||
|
block={item.block}
|
||||||
|
depth={depth}
|
||||||
|
onActionClick={onActionClick}
|
||||||
|
onBlockItemClick={onBlockItemClick}
|
||||||
|
onThoughtCardClick={onThoughtCardClick}
|
||||||
|
finallyBlockLabel={finallyBlockLabel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isThoughtItem(item)) {
|
||||||
|
return (
|
||||||
|
<ThoughtCard
|
||||||
|
key={item.thought.thought_id}
|
||||||
|
active={
|
||||||
|
isObserverThought(activeItem) &&
|
||||||
|
activeItem.thought_id === item.thought.thought_id
|
||||||
|
}
|
||||||
|
onClick={onThoughtCardClick}
|
||||||
|
thought={item.thought}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function WorkflowRunTimelineBlockItem({
|
function WorkflowRunTimelineBlockItem({
|
||||||
activeItem,
|
activeItem,
|
||||||
block,
|
block,
|
||||||
subItems,
|
subItems,
|
||||||
|
depth = 0,
|
||||||
onBlockItemClick,
|
onBlockItemClick,
|
||||||
onActionClick,
|
onActionClick,
|
||||||
onThoughtCardClick,
|
onThoughtCardClick,
|
||||||
@@ -98,241 +237,352 @@ function WorkflowRunTimelineBlockItem({
|
|||||||
|
|
||||||
// NOTE(jdo): want to put this back; await for now
|
// NOTE(jdo): want to put this back; await for now
|
||||||
const showDuration = false as const;
|
const showDuration = false as const;
|
||||||
|
const hasNestedChildren = subItems.length > 0;
|
||||||
|
const isLoopBlock = block.block_type === "for_loop";
|
||||||
|
const isConditionalBlock = block.block_type === "conditional";
|
||||||
|
const [childrenOpen, setChildrenOpen] = useState(!isConditionalBlock);
|
||||||
|
|
||||||
|
const loopIterationGroups = useMemo(
|
||||||
|
() => getLoopIterationGroups(subItems),
|
||||||
|
[subItems],
|
||||||
|
);
|
||||||
|
|
||||||
|
const loopValues = Array.isArray(block.loop_values) ? block.loop_values : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn({ "ml-3 border-l border-slate-700 pl-3": depth > 0 })}>
|
||||||
className={cn(
|
<div
|
||||||
"cursor-pointer space-y-4 rounded border border-slate-600 p-4",
|
className={cn(
|
||||||
{
|
"cursor-pointer space-y-4 rounded border border-slate-600 p-4",
|
||||||
"border-slate-50":
|
{
|
||||||
isWorkflowRunBlock(activeItem) &&
|
"border-slate-50":
|
||||||
activeItem.workflow_run_block_id === block.workflow_run_block_id,
|
isWorkflowRunBlock(activeItem) &&
|
||||||
},
|
activeItem.workflow_run_block_id === block.workflow_run_block_id,
|
||||||
)}
|
},
|
||||||
onClick={(event) => {
|
)}
|
||||||
event.stopPropagation();
|
onClick={(event) => {
|
||||||
onBlockItemClick(block);
|
event.stopPropagation();
|
||||||
}}
|
onBlockItemClick(block);
|
||||||
ref={refCallback}
|
}}
|
||||||
>
|
ref={refCallback}
|
||||||
<div className="space-y-2">
|
>
|
||||||
<div className="flex justify-between">
|
<div className="space-y-2">
|
||||||
<div className="flex gap-3">
|
<div className="flex justify-between">
|
||||||
<div className="rounded bg-slate-elevation5 p-2">
|
<div className="flex gap-3">
|
||||||
<WorkflowBlockIcon
|
<div className="rounded bg-slate-elevation5 p-2">
|
||||||
workflowBlockType={block.block_type}
|
<WorkflowBlockIcon
|
||||||
className="size-6"
|
workflowBlockType={block.block_type}
|
||||||
/>
|
className="size-6"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{workflowBlockTitle[block.block_type]}
|
{workflowBlockTitle[block.block_type]}
|
||||||
</span>
|
|
||||||
<span className="flex gap-2 text-xs text-slate-400">
|
|
||||||
{block.label}
|
|
||||||
</span>
|
|
||||||
{isFinallyBlock && (
|
|
||||||
<span className="w-fit rounded bg-amber-500 px-1.5 py-0.5 text-[10px] font-medium text-black">
|
|
||||||
Execute on any outcome
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span className="flex gap-2 text-xs text-slate-400">
|
||||||
</div>
|
{block.label}
|
||||||
</div>
|
</span>
|
||||||
<div className="flex gap-2">
|
{isFinallyBlock && (
|
||||||
{showFailureIndicator && (
|
<span className="w-fit rounded bg-amber-500 px-1.5 py-0.5 text-[10px] font-medium text-black">
|
||||||
<div className="self-start rounded bg-slate-elevation5 px-2 py-1">
|
Execute on any outcome
|
||||||
<CrossCircledIcon className="size-4 text-destructive" />
|
</span>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showSuccessIndicator && (
|
|
||||||
<div className="self-start rounded bg-slate-elevation5 px-2 py-1">
|
|
||||||
<CheckCircledIcon className="size-4 text-success" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col items-end gap-[1px]">
|
|
||||||
<div className="flex gap-1 self-start rounded bg-slate-elevation5 px-2 py-1">
|
|
||||||
{showDiagnosticLink ? (
|
|
||||||
<Link
|
|
||||||
to={`/tasks/${block.task_id}/diagnostics`}
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<ExternalLinkIcon className="size-4" />
|
|
||||||
<span className="text-xs">Diagnostics</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CubeIcon className="size-4" />
|
|
||||||
<span className="text-xs">Block</span>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{duration && showDuration && (
|
</div>
|
||||||
<div className="pr-[5px] text-xs text-[#00ecff]">
|
<div className="flex gap-2">
|
||||||
{duration}
|
{showFailureIndicator && (
|
||||||
|
<div className="self-start rounded bg-slate-elevation5 px-2 py-1">
|
||||||
|
<CrossCircledIcon className="size-4 text-destructive" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showSuccessIndicator && (
|
||||||
|
<div className="self-start rounded bg-slate-elevation5 px-2 py-1">
|
||||||
|
<CheckCircledIcon className="size-4 text-success" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col items-end gap-[1px]">
|
||||||
|
<div className="flex gap-1 self-start rounded bg-slate-elevation5 px-2 py-1">
|
||||||
|
{showDiagnosticLink ? (
|
||||||
|
<Link
|
||||||
|
to={`/tasks/${block.task_id}/diagnostics`}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<ExternalLinkIcon className="size-4" />
|
||||||
|
<span className="text-xs">Diagnostics</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CubeIcon className="size-4" />
|
||||||
|
<span className="text-xs">Block</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{duration && showDuration && (
|
||||||
|
<div className="pr-[5px] text-xs text-[#00ecff]">
|
||||||
|
{duration}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{block.description ? (
|
||||||
|
<div className="text-xs text-slate-400">{block.description}</div>
|
||||||
|
) : null}
|
||||||
|
{isLoopBlock && (
|
||||||
|
<div className="space-y-2 rounded bg-slate-elevation5 px-3 py-2 text-xs">
|
||||||
|
<div className="text-slate-300">
|
||||||
|
Iterable values:{" "}
|
||||||
|
<span className="font-medium text-slate-200">
|
||||||
|
{loopValues.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{loopValues.length > 0 && (
|
||||||
|
<div className="max-h-40 space-y-1 overflow-y-auto pr-1">
|
||||||
|
{loopValues.map((value, index) => (
|
||||||
|
<div key={index} className="text-slate-400">
|
||||||
|
<span className="mr-1 text-slate-500">[{index}]</span>
|
||||||
|
<code className="rounded bg-slate-elevation1 px-1 py-0.5 font-mono text-slate-300">
|
||||||
|
{truncateValue(stringifyTimelineValue(value), 120)}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
{block.block_type === "conditional" && block.executed_branch_id && (
|
||||||
{block.description ? (
|
<div className="space-y-2 rounded bg-slate-elevation5 px-3 py-2 text-xs">
|
||||||
<div className="text-xs text-slate-400">{block.description}</div>
|
{hasEvaluations(block.output) && block.output.evaluations ? (
|
||||||
) : null}
|
// New format: show all branch evaluations
|
||||||
{block.block_type === "conditional" && block.executed_branch_id && (
|
<div className="space-y-2">
|
||||||
<div className="space-y-2 rounded bg-slate-elevation5 px-3 py-2 text-xs">
|
{block.output.evaluations.map((evaluation, index) => (
|
||||||
{hasEvaluations(block.output) && block.output.evaluations ? (
|
<div
|
||||||
// New format: show all branch evaluations
|
key={evaluation.branch_id || index}
|
||||||
<div className="space-y-2">
|
className={cn(
|
||||||
{block.output.evaluations.map((evaluation, index) => (
|
"rounded border px-2 py-1.5",
|
||||||
<div
|
evaluation.is_matched
|
||||||
key={evaluation.branch_id || index}
|
? "border-success/50 bg-success/10"
|
||||||
className={cn(
|
: "border-slate-600 bg-slate-elevation3",
|
||||||
"rounded border px-2 py-1.5",
|
)}
|
||||||
evaluation.is_matched
|
>
|
||||||
? "border-success/50 bg-success/10"
|
{evaluation.is_default ? (
|
||||||
: "border-slate-600 bg-slate-elevation3",
|
<div className="text-slate-300">
|
||||||
)}
|
<span className="font-medium">Default branch</span>
|
||||||
>
|
|
||||||
{evaluation.is_default ? (
|
|
||||||
<div className="text-slate-300">
|
|
||||||
<span className="font-medium">Default branch</span>
|
|
||||||
{evaluation.is_matched && (
|
|
||||||
<span className="ml-2 text-success">✓ Matched</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-slate-400">
|
|
||||||
<code className="rounded bg-slate-elevation1 px-1 py-0.5 font-mono text-slate-300">
|
|
||||||
{evaluation.original_expression}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
{evaluation.rendered_expression &&
|
|
||||||
evaluation.rendered_expression !==
|
|
||||||
evaluation.original_expression && (
|
|
||||||
<div className="text-slate-400">
|
|
||||||
→ rendered to{" "}
|
|
||||||
<code className="rounded bg-slate-elevation1 px-1 py-0.5 font-mono text-slate-200">
|
|
||||||
{evaluation.rendered_expression}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-slate-400">evaluated to</span>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"font-medium",
|
|
||||||
evaluation.result
|
|
||||||
? "text-success"
|
|
||||||
: "text-red-400",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{evaluation.result ? "True" : "False"}
|
|
||||||
</span>
|
|
||||||
{evaluation.is_matched && (
|
{evaluation.is_matched && (
|
||||||
<span className="text-success">✓ Matched</span>
|
<span className="ml-2 text-success">✓ Matched</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<div className="space-y-1">
|
||||||
{evaluation.is_matched && evaluation.next_block_label && (
|
<div className="text-slate-400">
|
||||||
<div className="mt-1 text-slate-400">
|
<code className="rounded bg-slate-elevation1 px-1 py-0.5 font-mono text-slate-300">
|
||||||
→ Executing next block:{" "}
|
{evaluation.original_expression}
|
||||||
<span className="font-medium text-slate-300">
|
</code>
|
||||||
{evaluation.next_block_label}
|
</div>
|
||||||
</span>
|
{evaluation.rendered_expression &&
|
||||||
</div>
|
evaluation.rendered_expression !==
|
||||||
)}
|
evaluation.original_expression && (
|
||||||
|
<div className="text-slate-400">
|
||||||
|
-> rendered to{" "}
|
||||||
|
<code className="rounded bg-slate-elevation1 px-1 py-0.5 font-mono text-slate-200">
|
||||||
|
{evaluation.rendered_expression}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-slate-400">evaluated to</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"font-medium",
|
||||||
|
evaluation.result
|
||||||
|
? "text-success"
|
||||||
|
: "text-red-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{evaluation.result ? "True" : "False"}
|
||||||
|
</span>
|
||||||
|
{evaluation.is_matched && (
|
||||||
|
<span className="text-success">✓ Matched</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{evaluation.is_matched && evaluation.next_block_label && (
|
||||||
|
<div className="mt-1 text-slate-400">
|
||||||
|
-> Executing next block:{" "}
|
||||||
|
<span className="font-medium text-slate-300">
|
||||||
|
{evaluation.next_block_label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Fallback: old format without evaluations array
|
||||||
|
<>
|
||||||
|
{block.executed_branch_expression !== null &&
|
||||||
|
block.executed_branch_expression !== undefined ? (
|
||||||
|
<div className="text-slate-300">
|
||||||
|
Condition{" "}
|
||||||
|
<code className="rounded bg-slate-elevation3 px-1.5 py-0.5 font-mono text-slate-200">
|
||||||
|
{block.executed_branch_expression}
|
||||||
|
</code>{" "}
|
||||||
|
evaluated to{" "}
|
||||||
|
<span className="font-medium text-success">True</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-slate-300">
|
||||||
|
No conditions matched, executing default branch
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{block.executed_branch_next_block && (
|
||||||
|
<div className="text-slate-400">
|
||||||
|
-> Executing next block:{" "}
|
||||||
|
<span className="font-medium text-slate-300">
|
||||||
|
{block.executed_branch_next_block}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{block.block_type === "human_interaction" && (
|
||||||
|
<WorkflowRunHumanInteraction workflowRunBlock={block} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{actions.map((action, index) => {
|
||||||
|
return (
|
||||||
|
<ActionCard
|
||||||
|
key={action.action_id}
|
||||||
|
action={action}
|
||||||
|
active={
|
||||||
|
isAction(activeItem) &&
|
||||||
|
activeItem.action_id === action.action_id
|
||||||
|
}
|
||||||
|
index={actions.length - index}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const actionItem: ActionItem = {
|
||||||
|
block,
|
||||||
|
action,
|
||||||
|
};
|
||||||
|
onActionClick(actionItem);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{hasNestedChildren && isLoopBlock && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{loopIterationGroups.map((group, groupIndex) => {
|
||||||
|
const loopValueFromIterable =
|
||||||
|
group.index !== null ? loopValues[group.index] ?? null : null;
|
||||||
|
const iterationNumber =
|
||||||
|
group.index !== null
|
||||||
|
? group.index + 1
|
||||||
|
: loopIterationGroups.length - groupIndex;
|
||||||
|
const currentValuePreview = truncateValue(
|
||||||
|
stringifyTimelineValue(
|
||||||
|
loopValueFromIterable ?? group.currentValue,
|
||||||
|
),
|
||||||
|
140,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible
|
||||||
|
key={`${group.index ?? "unknown"}-${groupIndex}`}
|
||||||
|
defaultOpen={false}
|
||||||
|
>
|
||||||
|
<div className="rounded border border-slate-700 bg-slate-elevation4">
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<button
|
||||||
|
className="group flex w-full items-center justify-between gap-2 px-2 py-1 text-left"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<ChevronRightIcon className="size-4 text-slate-300 transition-transform group-data-[state=open]:rotate-90" />
|
||||||
|
<span className="text-xs text-slate-200">{`Iteration ${iterationNumber}`}</span>
|
||||||
|
</div>
|
||||||
|
<code className="max-w-[70%] truncate rounded bg-slate-elevation1 px-1 py-0.5 text-[11px] text-slate-300">
|
||||||
|
current_value: {currentValuePreview}
|
||||||
|
</code>
|
||||||
|
</button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<CollapsibleContent className="px-2 pb-2 pt-2">
|
||||||
</div>
|
<TimelineSubItems
|
||||||
) : (
|
items={group.items}
|
||||||
// Fallback: old format without evaluations array
|
activeItem={activeItem}
|
||||||
<>
|
depth={depth + 1}
|
||||||
{block.executed_branch_expression !== null &&
|
onActionClick={onActionClick}
|
||||||
block.executed_branch_expression !== undefined ? (
|
onBlockItemClick={onBlockItemClick}
|
||||||
<div className="text-slate-300">
|
onThoughtCardClick={onThoughtCardClick}
|
||||||
Condition{" "}
|
finallyBlockLabel={finallyBlockLabel}
|
||||||
<code className="rounded bg-slate-elevation3 px-1.5 py-0.5 font-mono text-slate-200">
|
/>
|
||||||
{block.executed_branch_expression}
|
</CollapsibleContent>
|
||||||
</code>{" "}
|
</Collapsible>
|
||||||
evaluated to{" "}
|
);
|
||||||
<span className="font-medium text-success">True</span>
|
})}
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-slate-300">
|
|
||||||
No conditions matched, executing default branch
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{block.executed_branch_next_block && (
|
|
||||||
<div className="text-slate-400">
|
|
||||||
→ Executing next block:{" "}
|
|
||||||
<span className="font-medium text-slate-300">
|
|
||||||
{block.executed_branch_next_block}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{block.block_type === "human_interaction" && (
|
{hasNestedChildren && isConditionalBlock && (
|
||||||
<WorkflowRunHumanInteraction workflowRunBlock={block} />
|
<Collapsible open={childrenOpen} onOpenChange={setChildrenOpen}>
|
||||||
)}
|
<div className="rounded border border-slate-700 bg-slate-elevation4 px-2 py-1.5">
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<button
|
||||||
|
className="flex w-full items-center justify-between gap-2 text-left"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-slate-200">
|
||||||
|
{childrenOpen ? (
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRightIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
<span>{`Executed branch blocks (${subItems.length})`}</span>
|
||||||
|
</div>
|
||||||
|
{block.executed_branch_next_block && (
|
||||||
|
<span className="text-[11px] text-slate-400">
|
||||||
|
next: {block.executed_branch_next_block}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</div>
|
||||||
|
<CollapsibleContent className="space-y-2 pt-2">
|
||||||
|
<TimelineSubItems
|
||||||
|
items={subItems}
|
||||||
|
activeItem={activeItem}
|
||||||
|
depth={depth + 1}
|
||||||
|
onActionClick={onActionClick}
|
||||||
|
onBlockItemClick={onBlockItemClick}
|
||||||
|
onThoughtCardClick={onThoughtCardClick}
|
||||||
|
finallyBlockLabel={finallyBlockLabel}
|
||||||
|
/>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
)}
|
||||||
|
|
||||||
{actions.map((action, index) => {
|
{hasNestedChildren && !isLoopBlock && !isConditionalBlock && (
|
||||||
return (
|
<TimelineSubItems
|
||||||
<ActionCard
|
items={subItems}
|
||||||
key={action.action_id}
|
activeItem={activeItem}
|
||||||
action={action}
|
depth={depth + 1}
|
||||||
active={
|
onActionClick={onActionClick}
|
||||||
isAction(activeItem) && activeItem.action_id === action.action_id
|
onBlockItemClick={onBlockItemClick}
|
||||||
}
|
onThoughtCardClick={onThoughtCardClick}
|
||||||
index={actions.length - index}
|
finallyBlockLabel={finallyBlockLabel}
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
const actionItem: ActionItem = {
|
|
||||||
block,
|
|
||||||
action,
|
|
||||||
};
|
|
||||||
onActionClick(actionItem);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
})}
|
</div>
|
||||||
{subItems.map((item) => {
|
|
||||||
if (isBlockItem(item)) {
|
|
||||||
return (
|
|
||||||
<WorkflowRunTimelineBlockItem
|
|
||||||
key={item.block.workflow_run_block_id}
|
|
||||||
subItems={item.children}
|
|
||||||
activeItem={activeItem}
|
|
||||||
block={item.block}
|
|
||||||
onActionClick={onActionClick}
|
|
||||||
onBlockItemClick={onBlockItemClick}
|
|
||||||
onThoughtCardClick={onThoughtCardClick}
|
|
||||||
finallyBlockLabel={finallyBlockLabel}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (isThoughtItem(item)) {
|
|
||||||
return (
|
|
||||||
<ThoughtCard
|
|
||||||
key={item.thought.thought_id}
|
|
||||||
active={
|
|
||||||
isObserverThought(activeItem) &&
|
|
||||||
activeItem.thought_id === item.thought.thought_id
|
|
||||||
}
|
|
||||||
onClick={onThoughtCardClick}
|
|
||||||
thought={item.thought}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4440,11 +4440,15 @@ class AgentDB(BaseAlchemyDB):
|
|||||||
workflow_run_block.task_id = task_id
|
workflow_run_block.task_id = task_id
|
||||||
if failure_reason:
|
if failure_reason:
|
||||||
workflow_run_block.failure_reason = failure_reason
|
workflow_run_block.failure_reason = failure_reason
|
||||||
if loop_values:
|
# Use `is not None` instead of truthiness checks so that falsy
|
||||||
|
# values like current_index=0, empty loop_values=[], or
|
||||||
|
# current_value="" are correctly persisted. Without this,
|
||||||
|
# the first loop iteration (index 0) loses its metadata.
|
||||||
|
if loop_values is not None:
|
||||||
workflow_run_block.loop_values = loop_values
|
workflow_run_block.loop_values = loop_values
|
||||||
if current_value:
|
if current_value is not None:
|
||||||
workflow_run_block.current_value = current_value
|
workflow_run_block.current_value = current_value
|
||||||
if current_index:
|
if current_index is not None:
|
||||||
workflow_run_block.current_index = current_index
|
workflow_run_block.current_index = current_index
|
||||||
if recipients:
|
if recipients:
|
||||||
workflow_run_block.recipients = recipients
|
workflow_run_block.recipients = recipients
|
||||||
|
|||||||
Reference in New Issue
Block a user