import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { listBrowserSessionProfiles } from '../../api'; export interface ParamHint { name: string; valueToReplace: string; type: 'string' | 'number' | 'boolean'; } interface SaveAsScriptDialogProps { /** The name of the recording (without .json extension). */ recordingName: string; onClose: () => void; /** Called with the new script filename (e.g. "my-script.js") after a successful compile. */ onSuccess: (scriptName: string) => void; } const SCRIPT_NAME_RE = /^[A-Za-z0-9_\-.]+$/; async function apiCompileScript(body: { recordingName: string; scriptName: string; description: string; sessionProfileId?: number; paramHints?: ParamHint[]; overwrite?: boolean; }): Promise<{ ok: boolean; scriptName: string }> { const { overwrite, ...rest } = body; const qs = overwrite ? '?overwrite=true' : ''; const res = await fetch(`/api/users/me/browser-macros/compile${qs}`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(rest), }); if (!res.ok) { const data = await res.json().catch(() => ({})); const err = new Error((data as { error?: string }).error ?? `HTTP ${res.status}`); (err as any).status = res.status; throw err; } return res.json(); } function emptyHint(): ParamHint { return { name: '', valueToReplace: '', type: 'string' }; } export function SaveAsScriptDialog({ recordingName, onClose, onSuccess }: SaveAsScriptDialogProps) { const qc = useQueryClient(); const [scriptName, setScriptName] = useState(recordingName); const [description, setDescription] = useState(''); const [sessionProfileId, setSessionProfileId] = useState(''); const [paramHints, setParamHints] = useState([]); const [overwrite, setOverwrite] = useState(false); const [validationError, setValidationError] = useState(null); const [conflictError, setConflictError] = useState(null); const { data: sessionProfiles = [], isLoading: profilesLoading } = useQuery({ queryKey: ['browser-session-profiles'], queryFn: listBrowserSessionProfiles, staleTime: 60 * 1000, }); const activeSessionProfiles = sessionProfiles.filter(p => p.status === 'active'); const compileMutation = useMutation({ mutationFn: apiCompileScript, onSuccess: (data) => { // Invalidate browser-macros list so the new file appears qc.invalidateQueries({ queryKey: ['userfolder', 'list', 'browser-macros'] }); onSuccess(data.scriptName); }, onError: (err: any) => { if (err.status === 409) { setConflictError('Script already exists; check the overwrite box and retry.'); } // Other errors surface via compileMutation.error }, }); function validate(): boolean { if (!scriptName.trim()) { setValidationError('Script name is required.'); return false; } const nameWithoutExt = scriptName.endsWith('.js') ? scriptName.slice(0, -3) : scriptName; if (!SCRIPT_NAME_RE.test(nameWithoutExt)) { setValidationError('Script name may only contain letters, numbers, dashes, underscores, and dots.'); return false; } if (!description.trim()) { setValidationError('Description is required.'); return false; } for (let i = 0; i < paramHints.length; i++) { const h = paramHints[i]!; if (!h.name.trim() || !h.valueToReplace.trim()) { setValidationError(`Param hint #${i + 1} must have a name and value to replace.`); return false; } } setValidationError(null); setConflictError(null); return true; } function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!validate()) return; const profileId = sessionProfileId.trim() !== '' ? parseInt(sessionProfileId, 10) : undefined; compileMutation.mutate({ recordingName, scriptName: scriptName.trim(), description: description.trim(), sessionProfileId: profileId !== undefined && !isNaN(profileId) ? profileId : undefined, paramHints: paramHints.length > 0 ? paramHints : undefined, overwrite, }); } function addHint() { setParamHints(prev => [...prev, emptyHint()]); } function updateHint(idx: number, patch: Partial) { setParamHints(prev => prev.map((h, i) => i === idx ? { ...h, ...patch } : h)); } function removeHint(idx: number) { setParamHints(prev => prev.filter((_, i) => i !== idx)); } const isSubmitting = compileMutation.isPending; const submitError = compileMutation.isError && !conflictError ? ((compileMutation.error as any)?.message ?? 'Compile failed') : null; return ( /* Backdrop */
{ if (e.target === e.currentTarget) onClose(); }} >
{/* Header */}
Save as Script
{/* Form */}
{/* Recording name (read-only) */}
{recordingName}.json
{/* Script name */}
setScriptName(e.target.value)} placeholder="my-script" className="px-3 py-2 rounded-md border border-hairline text-[13px] font-mono focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent" /> Alphanumeric, dashes, underscores, dots. A .js extension will be added automatically.
{/* Description */}