mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-06 19:35:44 +02:00
Let copilot add external triggers
feat: Enhance external trigger handling in Copilot - Added support for flexible schemas in external triggers, allowing configuration changes without stripping any data. - Introduced a new `onRequestTriggerSetup` callback in the Action component to facilitate trigger setup requests. - Implemented a modal for trigger configuration, improving user experience when setting up external triggers. - Updated the ComposioTriggerTypesPanel to auto-select trigger types based on initial configuration. This update significantly improves the management and setup of external triggers within the Copilot interface.
This commit is contained in:
parent
885a7f3753
commit
69eec4e41b
6 changed files with 402 additions and 19 deletions
|
|
@ -93,6 +93,10 @@ export function validateConfigChanges(configType: string, configChanges: Record<
|
|||
}).passthrough();
|
||||
break;
|
||||
}
|
||||
case 'external_trigger': {
|
||||
// External triggers have flexible schemas per provider; do not strip any config.
|
||||
return { changes: configChanges };
|
||||
}
|
||||
default:
|
||||
return { error: `Unknown config type: ${configType}` };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Modal, ModalBody, ModalContent, ModalHeader } from '@heroui/react';
|
||||
import { z } from 'zod';
|
||||
import { ZToolkit } from '@/src/application/lib/composio/types';
|
||||
import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type';
|
||||
import { Project } from '@/src/entities/models/project';
|
||||
import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit';
|
||||
import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel';
|
||||
import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm';
|
||||
import { ToolkitAuthModal } from '../../tools/components/ToolkitAuthModal';
|
||||
import { fetchProject } from '@/app/actions/project.actions';
|
||||
import { createComposioTriggerDeployment } from '@/app/actions/composio.actions';
|
||||
import { Button, Spinner } from '@heroui/react';
|
||||
|
||||
interface TriggerSetupModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
projectId: string;
|
||||
initialToolkitSlug?: string | null;
|
||||
initialTriggerTypeSlug?: string | null;
|
||||
initialTriggerConfig?: Record<string, unknown> | null;
|
||||
onCreated?: () => void;
|
||||
}
|
||||
|
||||
type Toolkit = z.infer<typeof ZToolkit>;
|
||||
type TriggerType = z.infer<typeof ComposioTriggerType>;
|
||||
type ProjectConfig = z.infer<typeof Project>;
|
||||
|
||||
export function TriggerSetupModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
projectId,
|
||||
initialToolkitSlug = null,
|
||||
initialTriggerTypeSlug = null,
|
||||
initialTriggerConfig = null,
|
||||
onCreated,
|
||||
}: TriggerSetupModalProps) {
|
||||
const [selectedToolkit, setSelectedToolkit] = useState<Toolkit | null>(null);
|
||||
const [selectedTriggerType, setSelectedTriggerType] = useState<TriggerType | null>(null);
|
||||
const [projectConfig, setProjectConfig] = useState<ProjectConfig | null>(null);
|
||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pendingTriggerTypeSlug, setPendingTriggerTypeSlug] = useState<string | null>(null);
|
||||
const [initialConfig, setInitialConfig] = useState<Record<string, unknown> | undefined>();
|
||||
|
||||
const loadProjectConfig = useCallback(async () => {
|
||||
try {
|
||||
const config = await fetchProject(projectId);
|
||||
setProjectConfig(config);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch project configuration', err);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setSelectedToolkit(null);
|
||||
setSelectedTriggerType(null);
|
||||
setShowAuthModal(false);
|
||||
setError(null);
|
||||
setPendingTriggerTypeSlug(initialTriggerTypeSlug);
|
||||
setInitialConfig(initialTriggerConfig ?? undefined);
|
||||
}, [initialTriggerConfig, initialTriggerTypeSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
resetState();
|
||||
void loadProjectConfig();
|
||||
}, [isOpen, loadProjectConfig, resetState]);
|
||||
|
||||
const requiresAuth = useMemo(() => {
|
||||
if (!selectedToolkit) return false;
|
||||
return !selectedToolkit.no_auth;
|
||||
}, [selectedToolkit]);
|
||||
|
||||
const hasActiveConnection = useMemo(() => {
|
||||
if (!selectedToolkit) return false;
|
||||
const status = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.status;
|
||||
return status === 'ACTIVE';
|
||||
}, [projectConfig, selectedToolkit]);
|
||||
|
||||
const handleSelectToolkit = useCallback((toolkit: Toolkit) => {
|
||||
setSelectedToolkit(toolkit);
|
||||
setSelectedTriggerType(null);
|
||||
setError(null);
|
||||
if (!initialToolkitSlug || toolkit.slug === initialToolkitSlug) {
|
||||
setPendingTriggerTypeSlug(initialTriggerTypeSlug);
|
||||
} else {
|
||||
setPendingTriggerTypeSlug(null);
|
||||
}
|
||||
}, [initialToolkitSlug, initialTriggerTypeSlug]);
|
||||
|
||||
const handleSelectTriggerType = useCallback((triggerType: TriggerType) => {
|
||||
setSelectedTriggerType(triggerType);
|
||||
setError(null);
|
||||
setPendingTriggerTypeSlug(null);
|
||||
if (requiresAuth && !hasActiveConnection) {
|
||||
setShowAuthModal(true);
|
||||
}
|
||||
}, [requiresAuth, hasActiveConnection]);
|
||||
|
||||
const handleAuthComplete = useCallback(async () => {
|
||||
await loadProjectConfig();
|
||||
setShowAuthModal(false);
|
||||
}, [loadProjectConfig]);
|
||||
|
||||
const handleSubmit = useCallback(async (triggerConfig: Record<string, unknown>) => {
|
||||
if (!selectedToolkit || !selectedTriggerType) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.id;
|
||||
if (!connectedAccountId) {
|
||||
setShowAuthModal(true);
|
||||
throw new Error('Connect this toolkit before creating a trigger.');
|
||||
}
|
||||
|
||||
await createComposioTriggerDeployment({
|
||||
projectId,
|
||||
triggerTypeSlug: selectedTriggerType.slug,
|
||||
connectedAccountId,
|
||||
triggerConfig,
|
||||
});
|
||||
|
||||
onCreated?.();
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
console.error('Failed to create trigger', err);
|
||||
setError(err?.message || 'Failed to create trigger. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [onClose, onCreated, projectConfig, projectId, selectedToolkit, selectedTriggerType]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
}, [isSubmitting, onClose]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
size="5xl"
|
||||
scrollBehavior="inside"
|
||||
classNames={{
|
||||
base: 'max-h-[90vh]'
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Set up External Trigger</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Follow the guided flow to authenticate and configure the trigger.
|
||||
</p>
|
||||
</ModalHeader>
|
||||
<ModalBody className="pb-6">
|
||||
{!selectedToolkit && (
|
||||
<SelectComposioToolkit
|
||||
key={isOpen ? 'toolkit-selector' : 'toolkit-selector-hidden'}
|
||||
projectId={projectId}
|
||||
tools={[]}
|
||||
onSelectToolkit={handleSelectToolkit}
|
||||
initialToolkitSlug={initialToolkitSlug}
|
||||
filterByTriggers={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedToolkit && !selectedTriggerType && (
|
||||
<ComposioTriggerTypesPanel
|
||||
key={selectedToolkit.slug}
|
||||
toolkit={selectedToolkit}
|
||||
onBack={() => setSelectedToolkit(null)}
|
||||
onSelectTriggerType={handleSelectTriggerType}
|
||||
initialTriggerTypeSlug={pendingTriggerTypeSlug}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedToolkit && selectedTriggerType && (!requiresAuth || hasActiveConnection) && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Button variant="light" size="sm" onPress={() => setSelectedTriggerType(null)}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
<TriggerConfigForm
|
||||
toolkit={selectedToolkit}
|
||||
triggerType={selectedTriggerType}
|
||||
onBack={() => setSelectedTriggerType(null)}
|
||||
onSubmit={handleSubmit}
|
||||
isSubmitting={isSubmitting}
|
||||
initialConfig={initialConfig}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedToolkit && selectedTriggerType && requiresAuth && !hasActiveConnection && !showAuthModal && (
|
||||
<div className="py-12 text-center space-y-4">
|
||||
<Spinner className="mx-auto" />
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Waiting for authentication to complete...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 rounded-md border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20 p-3 text-sm text-red-600 dark:text-red-300">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{selectedToolkit && (
|
||||
<ToolkitAuthModal
|
||||
isOpen={showAuthModal}
|
||||
onClose={() => setShowAuthModal(false)}
|
||||
projectId={projectId}
|
||||
toolkitSlug={selectedToolkit.slug}
|
||||
onComplete={handleAuthComplete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ export function Action({
|
|||
onApplied,
|
||||
externallyApplied = false,
|
||||
defaultExpanded = false,
|
||||
onRequestTriggerSetup,
|
||||
}: {
|
||||
msgIndex: number;
|
||||
actionIndex: number;
|
||||
|
|
@ -39,10 +40,12 @@ export function Action({
|
|||
onApplied?: () => void;
|
||||
externallyApplied?: boolean;
|
||||
defaultExpanded?: boolean;
|
||||
onRequestTriggerSetup?: (params: { action: z.infer<typeof CopilotAssistantMessageActionPart>['content']; msgIndex: number; actionIndex: number }) => void;
|
||||
}) {
|
||||
const { showPreview } = usePreviewModal();
|
||||
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||
const [appliedChanges, setAppliedChanges] = useState<Record<string, boolean>>({});
|
||||
const isExternalTriggerCreate = action.config_type === 'external_trigger' && action.action === 'create_new';
|
||||
|
||||
if (!action || typeof action !== 'object') {
|
||||
console.warn('Invalid action object:', action);
|
||||
|
|
@ -108,6 +111,10 @@ export function Action({
|
|||
|
||||
// Handle applying all changes - delegate to parent
|
||||
const handleApplyAll = () => {
|
||||
if (isExternalTriggerCreate) {
|
||||
onRequestTriggerSetup?.({ action, msgIndex, actionIndex });
|
||||
return;
|
||||
}
|
||||
// Mark all fields as applied locally for UI state
|
||||
const appliedKeys = Object.keys(action.config_changes).reduce((acc, key) => {
|
||||
acc[getAppliedChangeKey(msgIndex, actionIndex, key)] = true;
|
||||
|
|
@ -230,9 +237,9 @@ export function Action({
|
|||
onClick={() => handleApplyAll()}
|
||||
>
|
||||
<CheckIcon size={13} className={allApplied ? 'text-zinc-400' : 'text-green-600 group-hover:text-green-700'} />
|
||||
<span>{allApplied ? 'Applied' : 'Apply'}</span>
|
||||
<span>{allApplied ? 'Applied' : isExternalTriggerCreate ? 'Open setup' : 'Apply'}</span>
|
||||
</button>
|
||||
{action.action !== 'delete' && <button
|
||||
{action.action !== 'delete' && !isExternalTriggerCreate && <button
|
||||
className="flex items-center gap-1 rounded-full px-2 h-7 text-xs font-medium bg-transparent text-indigo-600 hover:text-indigo-700 transition-colors"
|
||||
onClick={handleViewDiff}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import MarkdownContent from "@/app/lib/components/markdown-content";
|
|||
import { MessageSquareIcon, EllipsisIcon, XIcon, CheckCheckIcon, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { CopilotMessage, CopilotAssistantMessage, CopilotAssistantMessageActionPart, TriggerSchemaForCopilot } from "@/src/entities/models/copilot";
|
||||
import { Action, StreamingAction } from './actions';
|
||||
import { TriggerSetupModal } from './TriggerSetupModal';
|
||||
import { useParsedBlocks } from "../use-parsed-blocks";
|
||||
import { validateConfigChanges } from "@/app/lib/client_utils";
|
||||
import { PreviewModalProvider } from '../../workflow/preview-modal';
|
||||
|
|
@ -224,6 +225,14 @@ function AssistantMessage({
|
|||
const triggersRef = useRef<CopilotTriggerType[] | undefined>(triggers);
|
||||
const pendingTriggerEditsRef = useRef<Map<string, CopilotTriggerType>>(new Map());
|
||||
const triggerUpdateCallbackRef = useRef<typeof onTriggersUpdated>(onTriggersUpdated);
|
||||
const [triggerSetupModal, setTriggerSetupModal] = useState<{
|
||||
action: z.infer<typeof CopilotAssistantMessageActionPart>['content'];
|
||||
actionIndex: number;
|
||||
messageIndex: number;
|
||||
initialToolkitSlug: string | null;
|
||||
initialTriggerTypeSlug: string | null;
|
||||
initialConfig?: Record<string, unknown>;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
triggersRef.current = triggers;
|
||||
|
|
@ -234,6 +243,82 @@ function AssistantMessage({
|
|||
triggerUpdateCallbackRef.current = onTriggersUpdated;
|
||||
}, [onTriggersUpdated]);
|
||||
|
||||
const refreshTriggers = useCallback(async () => {
|
||||
const callback = triggerUpdateCallbackRef.current;
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh triggers after Copilot action', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const requestTriggerSetup = useCallback((params: {
|
||||
action: z.infer<typeof CopilotAssistantMessageActionPart>['content'];
|
||||
actionIndex: number;
|
||||
messageIndex: number;
|
||||
}) => {
|
||||
const { action, actionIndex, messageIndex: msgIndex } = params;
|
||||
const changes = (action?.config_changes ?? {}) as Record<string, unknown>;
|
||||
const toStringOrNull = (value: unknown): string | null => {
|
||||
if (typeof value === 'string' && value.trim().length > 0) {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const deriveSlug = (primary: unknown, secondary: unknown, tertiary: unknown): string | null => {
|
||||
return toStringOrNull(primary) ?? toStringOrNull(secondary) ?? toStringOrNull(tertiary);
|
||||
};
|
||||
const toolkitSlug = deriveSlug(
|
||||
changes.toolkitSlug,
|
||||
changes.toolkit_slug,
|
||||
typeof changes.toolkit === 'object' && changes.toolkit !== null ? (changes.toolkit as any).slug : changes.toolkit
|
||||
);
|
||||
const triggerTypeSlug = deriveSlug(
|
||||
changes.triggerTypeSlug,
|
||||
changes.trigger_type_slug,
|
||||
typeof changes.triggerType === 'object' && changes.triggerType !== null ? (changes.triggerType as any).slug : changes.triggerType
|
||||
);
|
||||
const triggerConfigCandidate = (changes.triggerConfig ?? changes.trigger_config ?? changes.config) as unknown;
|
||||
const triggerConfig = typeof triggerConfigCandidate === 'object' && triggerConfigCandidate !== null
|
||||
? (triggerConfigCandidate as Record<string, unknown>)
|
||||
: undefined;
|
||||
|
||||
setTriggerSetupModal(prev => {
|
||||
if (prev && prev.actionIndex === actionIndex && prev.messageIndex === msgIndex) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
action,
|
||||
actionIndex,
|
||||
messageIndex: msgIndex,
|
||||
initialToolkitSlug: toolkitSlug,
|
||||
initialTriggerTypeSlug: triggerTypeSlug,
|
||||
initialConfig: triggerConfig,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleTriggerSetupCreated = useCallback(async () => {
|
||||
if (!triggerSetupModal) {
|
||||
return;
|
||||
}
|
||||
const index = triggerSetupModal.actionIndex;
|
||||
setAppliedActions(prev => {
|
||||
const next = new Set(prev);
|
||||
next.add(index);
|
||||
return next;
|
||||
});
|
||||
await refreshTriggers();
|
||||
setTriggerSetupModal(null);
|
||||
}, [refreshTriggers, triggerSetupModal]);
|
||||
|
||||
const handleTriggerSetupClosed = useCallback(() => {
|
||||
setTriggerSetupModal(null);
|
||||
}, []);
|
||||
|
||||
// parse actions from parts
|
||||
const parsed = useMemo(() => {
|
||||
const result: z.infer<typeof CopilotResponsePart>[] = [];
|
||||
|
|
@ -616,6 +701,15 @@ function AssistantMessage({
|
|||
}
|
||||
}
|
||||
|
||||
if (configType === 'external_trigger') {
|
||||
if (actionType === 'create_new') {
|
||||
if (typeof actionIndex === 'number') {
|
||||
requestTriggerSetup({ action, actionIndex, messageIndex });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ((configType === 'external_trigger' || configType === 'external') && actionType === 'delete') {
|
||||
const target = triggerList.find((trigger): trigger is Extract<CopilotTriggerType, { type: 'external' }> => {
|
||||
if (trigger.type !== 'external') {
|
||||
|
|
@ -646,19 +740,7 @@ function AssistantMessage({
|
|||
|
||||
console.warn('Unhandled trigger action from Copilot applyAction', action);
|
||||
return false;
|
||||
}, [projectId, parsed]);
|
||||
|
||||
const refreshTriggers = useCallback(async () => {
|
||||
const callback = triggerUpdateCallbackRef.current;
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh triggers after Copilot action', error);
|
||||
}
|
||||
}, []);
|
||||
}, [projectId, parsed, requestTriggerSetup, messageIndex]);
|
||||
|
||||
// Memoized handleApplyAll for useEffect dependencies
|
||||
const handleApplyAll = useCallback(async () => {
|
||||
|
|
@ -805,6 +887,7 @@ function AssistantMessage({
|
|||
|
||||
// Render all cards inline, not in a panel
|
||||
return (
|
||||
<>
|
||||
<div className="w-full">
|
||||
<div className="px-4 py-2.5 text-sm leading-relaxed text-gray-700 dark:text-gray-200">
|
||||
<div className="flex flex-col gap-2">
|
||||
|
|
@ -827,6 +910,9 @@ function AssistantMessage({
|
|||
onApplied={() => { void handleSingleApply(part.action, idx); }}
|
||||
externallyApplied={appliedActions.has(idx)}
|
||||
defaultExpanded={true}
|
||||
onRequestTriggerSetup={({ action, actionIndex }) =>
|
||||
requestTriggerSetup({ action, actionIndex, messageIndex })
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -845,6 +931,16 @@ function AssistantMessage({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TriggerSetupModal
|
||||
isOpen={Boolean(triggerSetupModal)}
|
||||
onClose={handleTriggerSetupClosed}
|
||||
projectId={projectId}
|
||||
initialToolkitSlug={triggerSetupModal?.initialToolkitSlug ?? null}
|
||||
initialTriggerTypeSlug={triggerSetupModal?.initialTriggerTypeSlug ?? null}
|
||||
initialTriggerConfig={triggerSetupModal?.initialConfig}
|
||||
onCreated={handleTriggerSetupCreated}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ interface ComposioTriggerTypesPanelProps {
|
|||
toolkit: z.infer<typeof ZToolkit>;
|
||||
onBack: () => void;
|
||||
onSelectTriggerType: (triggerType: z.infer<typeof ComposioTriggerType>) => void;
|
||||
initialTriggerTypeSlug?: string | null;
|
||||
}
|
||||
|
||||
type TriggerType = z.infer<typeof ComposioTriggerType>;
|
||||
|
|
@ -21,6 +22,7 @@ export function ComposioTriggerTypesPanel({
|
|||
toolkit,
|
||||
onBack,
|
||||
onSelectTriggerType,
|
||||
initialTriggerTypeSlug,
|
||||
}: ComposioTriggerTypesPanelProps) {
|
||||
const [triggerTypes, setTriggerTypes] = useState<TriggerType[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -28,6 +30,7 @@ export function ComposioTriggerTypesPanel({
|
|||
const [cursor, setCursor] = useState<string | null>(null);
|
||||
const [hasNextPage, setHasNextPage] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [autoSelected, setAutoSelected] = useState(false);
|
||||
|
||||
const loadTriggerTypes = useCallback(async (resetList = false, nextCursor?: string) => {
|
||||
try {
|
||||
|
|
@ -70,8 +73,20 @@ export function ComposioTriggerTypesPanel({
|
|||
|
||||
useEffect(() => {
|
||||
loadTriggerTypes(true);
|
||||
setAutoSelected(false);
|
||||
}, [loadTriggerTypes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialTriggerTypeSlug || autoSelected || triggerTypes.length === 0) {
|
||||
return;
|
||||
}
|
||||
const match = triggerTypes.find(triggerType => triggerType.slug === initialTriggerTypeSlug);
|
||||
if (match) {
|
||||
setAutoSelected(true);
|
||||
onSelectTriggerType(match);
|
||||
}
|
||||
}, [initialTriggerTypeSlug, triggerTypes, onSelectTriggerType, autoSelected]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
|
@ -215,4 +230,4 @@ export function ComposioTriggerTypesPanel({
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Button, Input, Card, CardBody, CardHeader } from '@heroui/react';
|
||||
import { ArrowLeft, ZapIcon, CheckCircleIcon } from 'lucide-react';
|
||||
import { z } from 'zod';
|
||||
|
|
@ -13,6 +13,7 @@ interface TriggerConfigFormProps {
|
|||
onBack: () => void;
|
||||
onSubmit: (config: Record<string, unknown>) => void;
|
||||
isSubmitting?: boolean;
|
||||
initialConfig?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface JsonSchemaProperty {
|
||||
|
|
@ -36,13 +37,36 @@ export function TriggerConfigForm({
|
|||
onBack,
|
||||
onSubmit,
|
||||
isSubmitting = false,
|
||||
initialConfig,
|
||||
}: TriggerConfigFormProps) {
|
||||
const [formData, setFormData] = useState<Record<string, string>>({});
|
||||
const [formData, setFormData] = useState<Record<string, string>>(() => {
|
||||
if (!initialConfig) {
|
||||
return {};
|
||||
}
|
||||
return Object.entries(initialConfig).reduce<Record<string, string>>((acc, [key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
acc[key] = String(value);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// Parse the JSON schema from triggerType.config
|
||||
const schema = triggerType.config as JsonSchema;
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialConfig) {
|
||||
return;
|
||||
}
|
||||
setFormData(Object.entries(initialConfig).reduce<Record<string, string>>((acc, [key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
acc[key] = String(value);
|
||||
}
|
||||
return acc;
|
||||
}, {}));
|
||||
}, [initialConfig, triggerType.slug]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
// Validate required fields
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
|
@ -267,4 +291,4 @@ export function TriggerConfigForm({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue