Skip to content

Commit 9f03ec1

Browse files
committed
feat(www): auto-save toggle, typed function inputs, flexible execute output
1 parent 7498c28 commit 9f03ec1

1 file changed

Lines changed: 56 additions & 11 deletions

File tree

www/src/components/FlowsPanel.tsx

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,25 @@ interface StoreEntry {
5353

5454

5555
function platformFnToDefinition(fn: FunctionNode): NodeDefinition {
56+
const inputs: Array<{ name: string; type: string }> = [];
57+
const secrets = (fn.requiredSecrets ?? []) as FunctionRequirement[];
58+
const configs = (fn.requiredConfigs ?? []) as FunctionRequirement[];
59+
60+
for (const s of secrets) {
61+
if (s.name) inputs.push({ name: s.name, type: 'string' });
62+
}
63+
for (const c of configs) {
64+
if (c.name) inputs.push({ name: c.name, type: 'string' });
65+
}
66+
// Fallback: if no fields declared, expose a generic payload input
67+
if (inputs.length === 0) inputs.push({ name: 'payload', type: 'json' });
68+
5669
return {
5770
context: fn.scope || 'platform',
5871
name: fn.taskIdentifier || fn.name || '',
5972
category: 'functions',
6073
description: fn.description || undefined,
61-
inputs: [{ name: 'payload', type: 'json' }],
74+
inputs,
6275
outputs: [{ name: 'result', type: 'json' }],
6376
icon: fn.isInvocable ? 'zap' : 'circle',
6477
};
@@ -130,6 +143,8 @@ const FUNCTION_FIELDS = {
130143
isBuiltIn: true,
131144
scope: true,
132145
description: true,
146+
requiredSecrets: true,
147+
requiredConfigs: true,
133148
} as const;
134149

135150
const STORE_FIELDS = { id: true, name: true, hash: true } as const;
@@ -336,7 +351,9 @@ async function deleteStore(id: string): Promise<void> {
336351

337352
async function importAndExecuteGraph(
338353
graph: Graph,
339-
inputPayload: Record<string, unknown> = {}
354+
inputPayload: Record<string, unknown> = {},
355+
outputNode: string | null = null,
356+
outputPort: string | null = null,
340357
): Promise<{ graphId: string; executionId: string }> {
341358
const endpoint = '/graphql/compute';
342359

@@ -354,18 +371,15 @@ async function importAndExecuteGraph(
354371
});
355372
const graphId = importData.platformImportGraphJson.result;
356373

374+
const execInput: Record<string, unknown> = { graphId, inputPayload };
375+
if (outputNode) execInput.outputNode = outputNode;
376+
if (outputPort) execInput.outputPort = outputPort;
377+
357378
const execData = await gqlFetch(endpoint, `
358379
mutation ($input: PlatformStartExecutionInput!) {
359380
platformStartExecution(input: $input) { result }
360381
}
361-
`, {
362-
input: {
363-
graphId,
364-
inputPayload,
365-
outputNode: 'output_result',
366-
outputPort: 'value',
367-
},
368-
});
382+
`, { input: execInput });
369383
const executionId = execData.platformStartExecution.result;
370384

371385
return { graphId, executionId };
@@ -443,6 +457,8 @@ export function FlowsPanel() {
443457
const [isLoadingFlow, setIsLoadingFlow] = useState(false);
444458
const [collapsedCategories, setCollapsedCategories] = useState<Set<string>>(new Set());
445459
const [saveStatus, setSaveStatus] = useState<string | null>(null);
460+
const [autoSave, setAutoSave] = useState(false);
461+
const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
446462
const [executionState, setExecutionState] = useState<ExecutionState | null>(null);
447463
const [isExecuting, setIsExecuting] = useState(false);
448464
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
@@ -541,6 +557,7 @@ export function FlowsPanel() {
541557
setCurrentGraph(graph);
542558
}, []);
543559

560+
544561
const handleEvaluate = useCallback(async () => {
545562
const graph = graphRef.current;
546563
if (!graph.nodes.length) return;
@@ -589,7 +606,14 @@ export function FlowsPanel() {
589606
}
590607
}
591608

592-
const { executionId } = await importAndExecuteGraph(graph, inputPayload);
609+
// Auto-detect output node — use first graphOutput if present, otherwise omit
610+
const outputNode = graph.nodes.find(n => n.type === 'graphOutput');
611+
const { executionId } = await importAndExecuteGraph(
612+
graph,
613+
inputPayload,
614+
outputNode?.name ?? null,
615+
outputNode ? 'value' : null,
616+
);
593617

594618
// Initialize node states — mark all non-boundary nodes as pending
595619
const initialNodeStates: Record<string, NodeState> = {};
@@ -671,6 +695,18 @@ export function FlowsPanel() {
671695
}
672696
}, [flowName, stores.length, activeStoreId, activeCommitId, refetchStores]);
673697

698+
// Auto-save: debounce 1s after each graph edit
699+
useEffect(() => {
700+
if (!autoSave || !activeStoreId) return;
701+
if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
702+
autoSaveTimerRef.current = setTimeout(() => {
703+
handleSave();
704+
}, 1000);
705+
return () => {
706+
if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
707+
};
708+
}, [currentGraph, autoSave, activeStoreId, handleSave]);
709+
674710
const handleNew = useCallback(() => {
675711
setCurrentGraph({ ...DEFAULT_GRAPH, name: '' });
676712
setFlowName('');
@@ -743,6 +779,15 @@ export function FlowsPanel() {
743779
{isSaving ? <RefreshCw size={14} className="animate-spin" /> : <Save size={14} />}
744780
{isSaving ? 'Saving...' : 'Save'}
745781
</button>
782+
<label className="flex items-center gap-1.5 text-xs text-zinc-400 cursor-pointer select-none">
783+
<input
784+
type="checkbox"
785+
checked={autoSave}
786+
onChange={(e) => setAutoSave(e.target.checked)}
787+
className="accent-green-500 w-3.5 h-3.5"
788+
/>
789+
Auto
790+
</label>
746791
<button
747792
onClick={handleEvaluate}
748793
disabled={isEvaluating}

0 commit comments

Comments
 (0)