merge EditTemplateModal into RunAgentModal

This commit is contained in:
Reinier van der Leer
2025-12-02 23:16:37 +01:00
parent a72d49cd15
commit c6ce7b9fe9
8 changed files with 216 additions and 323 deletions

View File

@@ -69,7 +69,6 @@ export function NewAgentLibraryView() {
</Button>
}
agent={agent}
agentId={agent.id.toString()}
onRunCreated={(execution) => handleSelectRun(execution.id)}
onScheduleCreated={(schedule) =>
handleSelectRun(`schedule:${schedule.id}`)

View File

@@ -1,267 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { usePatchV2UpdateAnExistingPreset } from "@/app/api/__generated__/endpoints/presets/presets";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { AgentCostSection } from "./RunAgentModal/components/AgentCostSection/AgentCostSection";
import { AgentSectionHeader } from "./RunAgentModal/components/AgentSectionHeader/AgentSectionHeader";
import { AgentDetails } from "./RunAgentModal/components/AgentDetails/AgentDetails";
import { ModalHeader } from "./RunAgentModal/components/ModalHeader/ModalHeader";
import { ModalRunSection } from "./RunAgentModal/components/ModalRunSection/ModalRunSection";
import { RunAgentModalContextProvider } from "./RunAgentModal/context";
interface Props {
triggerSlot: React.ReactNode;
agent: LibraryAgent;
preset: LibraryAgentPreset;
onSaved?: (updatedPreset: LibraryAgentPreset) => void;
}
export function EditTemplateModal({
triggerSlot,
agent,
preset,
onSaved,
}: Props) {
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false);
const [presetName, setPresetName] = useState(preset.name);
const [presetDescription, setPresetDescription] = useState(
preset.description,
);
const [inputValues, setInputValues] = useState<Record<string, any>>(
preset.inputs || {},
);
const [inputCredentials, setInputCredentials] = useState<Record<string, any>>(
preset.credentials || {},
);
// Reset form when preset changes
useEffect(() => {
setPresetName(preset.name);
setPresetDescription(preset.description);
setInputValues(preset.inputs || {});
setInputCredentials(preset.credentials || {});
}, [preset]);
// Update preset mutation
const updatePresetMutation = usePatchV2UpdateAnExistingPreset({
mutation: {
onSuccess: (response) => {
if (response.status === 200) {
toast({
title: "Template updated successfully",
variant: "default",
});
setIsOpen(false);
onSaved?.(response.data as unknown as LibraryAgentPreset);
}
},
onError: (error: any) => {
toast({
title: "Failed to update template",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
// Input schema validation (reusing logic from useAgentRunModal)
const agentInputSchema = agent.trigger_setup_info
? agent.trigger_setup_info.config_schema
: agent.input_schema;
const agentInputFields = (() => {
if (
!agentInputSchema ||
typeof agentInputSchema !== "object" ||
!("properties" in agentInputSchema) ||
!agentInputSchema.properties
) {
return {};
}
const properties = agentInputSchema.properties as Record<string, any>;
return Object.fromEntries(
Object.entries(properties).filter(
([_, subSchema]: [string, any]) => !subSchema.hidden,
),
);
})();
const agentCredentialsInputFields = (() => {
if (
!agent.credentials_input_schema ||
typeof agent.credentials_input_schema !== "object" ||
!("properties" in agent.credentials_input_schema) ||
!agent.credentials_input_schema.properties
) {
return {} as Record<string, any>;
}
return agent.credentials_input_schema.properties as Record<string, any>;
})();
const hasAnySetupFields =
Object.keys(agentInputFields || {}).length > 0 ||
Object.keys(agentCredentialsInputFields || {}).length > 0;
function handleInputChange(key: string, value: string) {
setInputValues((prev) => ({
...prev,
[key]: value,
}));
}
function handleCredentialsChange(key: string, value: any | undefined) {
setInputCredentials((prev) => {
const next = { ...prev } as Record<string, any>;
if (value === undefined) {
delete next[key];
return next;
}
next[key] = value;
return next;
});
}
function handleSetOpen(open: boolean) {
setIsOpen(open);
}
function handleSave() {
updatePresetMutation.mutate({
presetId: preset.id,
data: {
name: presetName,
description: presetDescription,
inputs: inputValues,
credentials: inputCredentials,
},
});
}
const hasChanges =
presetName !== preset.name ||
presetDescription !== preset.description ||
JSON.stringify(inputValues) !== JSON.stringify(preset.inputs || {}) ||
JSON.stringify(inputCredentials) !==
JSON.stringify(preset.credentials || {});
return (
<Dialog
controlled={{ isOpen, set: handleSetOpen }}
styling={{ maxWidth: "600px", maxHeight: "90vh" }}
>
<Dialog.Trigger>{triggerSlot}</Dialog.Trigger>
<Dialog.Content>
<div className="flex h-full flex-col pb-4">
{/* Header */}
<div className="flex-shrink-0">
<ModalHeader agent={agent} />
<AgentCostSection flowId={agent.graph_id} />
</div>
{/* Scrollable content */}
<div className="flex-1 pr-1" style={{ scrollbarGutter: "stable" }}>
{/* Template Info Section */}
<div className="mt-10">
<AgentSectionHeader title="Template Information" />
<div className="mb-10 mt-4 space-y-4">
<div className="flex flex-col space-y-2">
<label className="text-sm font-medium">Template Name</label>
<Input
id="template_name"
label="Template Name"
size="small"
hideLabel
value={presetName}
placeholder="Enter template name"
onChange={(e) => setPresetName(e.target.value)}
/>
</div>
<div className="flex flex-col space-y-2">
<label className="text-sm font-medium">
Template Description
</label>
<Input
id="template_description"
label="Template Description"
size="small"
hideLabel
value={presetDescription}
placeholder="Enter template description"
onChange={(e) => setPresetDescription(e.target.value)}
/>
</div>
</div>
</div>
{/* Setup Section */}
{hasAnySetupFields ? (
<div className="mt-8">
<RunAgentModalContextProvider
value={{
agent,
defaultRunType: "manual", // Always manual for templates
presetName,
setPresetName,
presetDescription,
setPresetDescription,
inputValues,
setInputValue: handleInputChange,
agentInputFields,
inputCredentials,
setInputCredentialsValue: handleCredentialsChange,
agentCredentialsInputFields,
}}
>
<>
<AgentSectionHeader title="Template Setup" />
<ModalRunSection />
</>
</RunAgentModalContextProvider>
</div>
) : null}
{/* Agent Details Section */}
<div className="mt-8">
<AgentSectionHeader title="Agent Details" />
<AgentDetails agent={agent} />
</div>
</div>
</div>
<Dialog.Footer
className="fixed bottom-1 left-0 z-10 w-full bg-white p-4"
style={{ boxShadow: "0px -8px 10px white" }}
>
<div className="flex items-center justify-end gap-3">
<Button
variant="secondary"
onClick={() => setIsOpen(false)}
disabled={updatePresetMutation.isPending}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSave}
disabled={
!hasChanges ||
updatePresetMutation.isPending ||
!presetName.trim()
}
>
{updatePresetMutation.isPending ? "Saving..." : "Save Changes"}
</Button>
</div>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -5,6 +5,7 @@ import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutio
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { AlarmIcon } from "@phosphor-icons/react";
import { useState } from "react";
@@ -21,7 +22,6 @@ import { useAgentRunModal } from "./useAgentRunModal";
interface Props {
triggerSlot: React.ReactNode;
agent: LibraryAgent;
agentId: string;
agentVersion?: number;
initialInputValues?: Record<string, any>;
initialInputCredentials?: Record<string, any>;
@@ -30,6 +30,10 @@ interface Props {
onRunCreated?: (execution: GraphExecutionMeta) => void;
onTriggerSetup?: (preset: LibraryAgentPreset) => void;
onScheduleCreated?: (schedule: GraphExecutionJobInfo) => void;
editMode?: {
preset: LibraryAgentPreset;
onSaved?: (updatedPreset: LibraryAgentPreset) => void;
};
}
export function RunAgentModal({
@@ -42,6 +46,7 @@ export function RunAgentModal({
onRunCreated,
onTriggerSetup,
onScheduleCreated,
editMode,
}: Props) {
const {
// UI state
@@ -65,6 +70,11 @@ export function RunAgentModal({
setPresetName,
setPresetDescription,
// Edit mode
hasChanges,
isUpdatingPreset,
handleSave,
// Validation/readiness
allRequiredInputsAreSet,
@@ -81,10 +91,12 @@ export function RunAgentModal({
} = useAgentRunModal(agent, {
onRun: onRunCreated,
onSetupTrigger: onTriggerSetup,
onScheduleCreated,
initialInputValues,
initialInputCredentials,
initialPresetName,
initialPresetDescription,
editMode,
});
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
@@ -130,6 +142,8 @@ export function RunAgentModal({
onScheduleCreated?.(schedule);
}
const templateOrTrigger = agent.trigger_setup_info ? "Trigger" : "Template";
return (
<>
<Dialog
@@ -147,23 +161,64 @@ export function RunAgentModal({
{/* Scrollable content */}
<div className="flex-1 pr-1" style={{ scrollbarGutter: "stable" }}>
{/* Template Info Section (Edit Mode Only) */}
{editMode && (
<div className="mt-10">
<AgentSectionHeader
title={`${templateOrTrigger} Information`}
/>
<div className="mb-10 mt-4 space-y-4">
<div className="flex flex-col space-y-2">
<label className="text-sm font-medium">
{templateOrTrigger} Name
</label>
<Input
id="template_name"
label="Template Name"
size="small"
hideLabel
value={presetName}
placeholder="Enter template name"
onChange={(e) => setPresetName(e.target.value)}
/>
</div>
<div className="flex flex-col space-y-2">
<label className="text-sm font-medium">
{templateOrTrigger} Description
</label>
<Input
id="template_description"
label="Template Description"
size="small"
hideLabel
value={presetDescription}
placeholder="Enter template description"
onChange={(e) => setPresetDescription(e.target.value)}
/>
</div>
</div>
</div>
)}
{/* Setup Section */}
<div className="mt-10">
<div className={editMode ? "mt-8" : "mt-10"}>
{hasAnySetupFields ? (
<RunAgentModalContextProvider
value={{
agent,
defaultRunType,
presetName,
setPresetName,
presetDescription,
setPresetDescription,
inputValues,
setInputValue: handleInputChange,
agentInputFields,
inputCredentials,
setInputCredentialsValue: handleCredentialsChange,
agentCredentialsInputFields,
presetEditMode: Boolean(
editMode || agent.trigger_setup_info,
),
presetName,
setPresetName,
presetDescription,
setPresetDescription,
}}
>
<>
@@ -192,27 +247,51 @@ export function RunAgentModal({
style={{ boxShadow: "0px -8px 10px white" }}
>
<div className="flex items-center justify-end gap-3">
{(defaultRunType == "manual" || defaultRunType == "schedule") && (
<Button
variant="secondary"
onClick={handleOpenScheduleModal}
disabled={
isExecuting ||
isSettingUpTrigger ||
!allRequiredInputsAreSet
}
>
<AlarmIcon size={16} />
Schedule Agent
</Button>
{editMode ? (
<>
<Button
variant="secondary"
onClick={() => setIsOpen(false)}
disabled={isUpdatingPreset}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSave}
disabled={
!hasChanges || isUpdatingPreset || !presetName.trim()
}
>
{isUpdatingPreset ? "Saving..." : "Save Changes"}
</Button>
</>
) : (
<>
{(defaultRunType == "manual" ||
defaultRunType == "schedule") && (
<Button
variant="secondary"
onClick={handleOpenScheduleModal}
disabled={
isExecuting ||
isSettingUpTrigger ||
!allRequiredInputsAreSet
}
>
<AlarmIcon size={16} />
Schedule Agent
</Button>
)}
<RunActions
defaultRunType={defaultRunType}
onRun={handleRun}
isExecuting={isExecuting}
isSettingUpTrigger={isSettingUpTrigger}
isRunReady={allRequiredInputsAreSet}
/>
</>
)}
<RunActions
defaultRunType={defaultRunType}
onRun={handleRun}
isExecuting={isExecuting}
isSettingUpTrigger={isSettingUpTrigger}
isRunReady={allRequiredInputsAreSet}
/>
</div>
{(defaultRunType == "manual" || defaultRunType == "schedule") && (
<ScheduleAgentModal

View File

@@ -13,16 +13,17 @@ export function ModalRunSection() {
const {
agent,
defaultRunType,
presetName,
setPresetName,
presetDescription,
setPresetDescription,
inputValues,
setInputValue,
agentInputFields,
inputCredentials,
setInputCredentialsValue,
agentCredentialsInputFields,
presetEditMode,
presetName,
setPresetName,
presetDescription,
setPresetDescription,
} = useRunAgentModalContext();
return (
@@ -30,7 +31,7 @@ export function ModalRunSection() {
{defaultRunType === "automatic-trigger" && <WebhookTriggerBanner />}
{/* Preset/Trigger fields */}
{defaultRunType === "automatic-trigger" && (
{presetEditMode && (
<div className="flex flex-col gap-4">
<div className="flex flex-col space-y-2">
<label className="flex items-center gap-1 text-sm font-medium">

View File

@@ -4,14 +4,9 @@ import React, { createContext, useContext } from "react";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { RunVariant } from "./useAgentRunModal";
export interface RunAgentModalContextValue {
export type RunAgentModalContextValue = {
agent: LibraryAgent;
defaultRunType: RunVariant;
// Preset / Trigger
presetName: string;
setPresetName: (value: string) => void;
presetDescription: string;
setPresetDescription: (value: string) => void;
// Inputs
inputValues: Record<string, any>;
setInputValue: (key: string, value: any) => void;
@@ -20,7 +15,14 @@ export interface RunAgentModalContextValue {
inputCredentials: Record<string, any>;
setInputCredentialsValue: (key: string, value: any | undefined) => void;
agentCredentialsInputFields: Record<string, any>;
}
// Trigger / Preset fields
presetEditMode: boolean; // determines whether to show the name and description fields
presetName: string;
setPresetName: (value: string) => void;
presetDescription: string;
setPresetDescription: (value: string) => void;
};
const RunAgentModalContext = createContext<RunAgentModalContextValue | null>(
null,

View File

@@ -14,6 +14,7 @@ import {
import {
getGetV2ListPresetsQueryKey,
usePostV2SetupTrigger,
usePatchV2UpdateAnExistingPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
@@ -31,11 +32,15 @@ export type RunVariant =
interface UseAgentRunModalCallbacks {
onRun?: (execution: GraphExecutionMeta) => void;
onSetupTrigger?: (preset: LibraryAgentPreset) => void;
onCreateSchedule?: (schedule: GraphExecutionJobInfo) => void;
onScheduleCreated?: (schedule: GraphExecutionJobInfo) => void;
initialInputValues?: Record<string, any>;
initialInputCredentials?: Record<string, any>;
initialPresetName?: string;
initialPresetDescription?: string;
editMode?: {
preset: LibraryAgentPreset;
onSaved?: (updatedPreset: LibraryAgentPreset) => void;
};
}
export function useAgentRunModal(
@@ -47,16 +52,20 @@ export function useAgentRunModal(
const [isOpen, setIsOpen] = useState(false);
const [showScheduleView, setShowScheduleView] = useState(false);
const [inputValues, setInputValues] = useState<Record<string, any>>(
callbacks?.initialInputValues || {},
callbacks?.initialInputValues || callbacks?.editMode?.preset?.inputs || {},
);
const [inputCredentials, setInputCredentials] = useState<Record<string, any>>(
callbacks?.initialInputCredentials || {},
callbacks?.initialInputCredentials ||
callbacks?.editMode?.preset?.credentials ||
{},
);
const [presetName, setPresetName] = useState<string>(
callbacks?.initialPresetName || "",
callbacks?.initialPresetName || callbacks?.editMode?.preset?.name || "",
);
const [presetDescription, setPresetDescription] = useState<string>(
callbacks?.initialPresetDescription || "",
callbacks?.initialPresetDescription ||
callbacks?.editMode?.preset?.description ||
"",
);
const defaultScheduleName = useMemo(() => `Run ${agent.name}`, [agent.name]);
const [scheduleName, setScheduleName] = useState(defaultScheduleName);
@@ -116,7 +125,7 @@ export function useAgentRunModal(
toast({
title: "Schedule created",
});
callbacks?.onCreateSchedule?.(response.data);
callbacks?.onScheduleCreated?.(response.data);
// Invalidate schedules list for this graph
queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryKey(
@@ -172,11 +181,36 @@ export function useAgentRunModal(
},
});
// Input schema validation
const agentInputSchema = useMemo(
() => agent.input_schema || { properties: {}, required: [] },
[agent.input_schema],
);
// Edit mode mutation for updating presets
const updatePresetMutation = usePatchV2UpdateAnExistingPreset({
mutation: {
onSuccess: (response) => {
if (response.status === 200) {
toast({
title: "Template updated successfully",
variant: "default",
});
setIsOpen(false);
callbacks?.editMode?.onSaved?.(response.data);
}
},
onError: (error: any) => {
toast({
title: "Failed to update template",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
// Input schema validation (use trigger schema for triggered agents)
const agentInputSchema = useMemo(() => {
if (agent.trigger_setup_info?.config_schema) {
return agent.trigger_setup_info.config_schema;
}
return agent.input_schema || { properties: {}, required: [] };
}, [agent.input_schema, agent.trigger_setup_info]);
const agentInputFields = useMemo(() => {
if (
@@ -367,6 +401,8 @@ export function useAgentRunModal(
createScheduleMutation,
toast,
userTimezone,
presetName,
completeOnboardingStep,
]);
function handleShowSchedule() {
@@ -393,6 +429,47 @@ export function useAgentRunModal(
setCronExpression(expression);
}
// Edit mode save handler
const handleSave = useCallback(() => {
if (!callbacks?.editMode?.preset) return;
updatePresetMutation.mutate({
presetId: callbacks.editMode.preset.id,
data: {
name: presetName,
description: presetDescription,
inputs: inputValues,
credentials: inputCredentials,
},
});
}, [
callbacks?.editMode?.preset,
presetName,
presetDescription,
inputValues,
inputCredentials,
updatePresetMutation,
]);
// Check if there are changes in edit mode
const hasChanges = useMemo(() => {
if (!callbacks?.editMode?.preset) return false;
const preset = callbacks.editMode.preset;
return (
presetName !== preset.name ||
presetDescription !== preset.description ||
JSON.stringify(inputValues) !== JSON.stringify(preset.inputs || {}) ||
JSON.stringify(inputCredentials) !==
JSON.stringify(preset.credentials || {})
);
}, [
callbacks?.editMode?.preset,
presetName,
presetDescription,
inputValues,
inputCredentials,
]);
const hasInputFields = useMemo(() => {
return Object.keys(agentInputFields).length > 0;
}, [agentInputFields]);
@@ -437,6 +514,7 @@ export function useAgentRunModal(
isExecuting: executeGraphMutation.isPending,
isCreatingSchedule: createScheduleMutation.isPending,
isSettingUpTrigger: setupTriggerMutation.isPending,
isUpdatingPreset: updatePresetMutation.isPending,
// Actions
handleRun,
@@ -445,5 +523,7 @@ export function useAgentRunModal(
handleGoBack,
handleSetScheduleName,
handleSetCronExpression,
handleSave,
hasChanges,
};
}

View File

@@ -46,7 +46,6 @@ export function EmptyAgentRuns({ agent }: Props) {
</Button>
}
agent={agent}
agentId={agent.id.toString()}
/>
</div>
</div>

View File

@@ -27,7 +27,6 @@ import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutio
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
import { EditTemplateModal } from "../../modals/EditTemplateModal";
import { RunAgentModal } from "../../modals/RunAgentModal/RunAgentModal";
import { okData } from "@/app/api/helpers";
@@ -192,7 +191,6 @@ export function SelectedTemplateView({
</Button>
}
agent={agent}
agentId={agent.id}
initialInputValues={preset.inputs || {}}
initialInputCredentials={preset.credentials || {}}
initialPresetName={preset.name}
@@ -201,7 +199,7 @@ export function SelectedTemplateView({
onScheduleCreated={onCreateSchedule}
/>
) : null}
<EditTemplateModal
<RunAgentModal
triggerSlot={
<Button
variant="secondary"
@@ -212,8 +210,10 @@ export function SelectedTemplateView({
</Button>
}
agent={agent}
preset={preset}
onSaved={onSave}
editMode={{
preset,
onSaved: onSave,
}}
/>
<Button
// TODO: add confirmation modal before deleting