import type { Folder, HttpRequest } from "@yaakapp-internal/models"; import { foldersAtom, httpRequestsAtom } from "@yaakapp-internal/models"; import type { FormInput, FormInputCheckbox, FormInputEditor, FormInputFile, FormInputHttpRequest, FormInputKeyValue, FormInputSelect, FormInputText, JsonPrimitive, } from "@yaakapp-internal/plugins"; import classNames from "classnames"; import { useAtomValue } from "jotai"; import { useCallback, useEffect, useMemo } from "react"; import { useActiveRequest } from "../hooks/useActiveRequest"; import { useRandomKey } from "../hooks/useRandomKey"; import { capitalize } from "../lib/capitalize"; import { showDialog } from "../lib/dialog"; import { resolvedModelName } from "../lib/resolvedModelName"; import { Banner } from "./core/Banner"; import { Checkbox } from "./core/Checkbox"; import { DetailsBanner } from "./core/DetailsBanner"; import { Editor } from "./core/Editor/LazyEditor"; import { IconButton } from "./core/IconButton"; import type { InputProps } from "./core/Input"; import { Input } from "./core/Input"; import { Label } from "./core/Label"; import type { Pair } from "./core/PairEditor"; import { PairEditor } from "./core/PairEditor"; import { PlainInput } from "./core/PlainInput"; import { Select } from "./core/Select"; import { VStack } from "./core/Stacks"; import { Markdown } from "./Markdown"; import { SelectFile } from "./SelectFile"; export const DYNAMIC_FORM_NULL_ARG = "__NULL__"; const INPUT_SIZE = "sm"; interface Props { inputs: FormInput[] | undefined | null; onChange: (value: T) => void; data: T; autocompleteFunctions?: boolean; autocompleteVariables?: boolean; stateKey: string; className?: string; disabled?: boolean; } export function DynamicForm>({ inputs, data, onChange, autocompleteVariables, autocompleteFunctions, stateKey, className, disabled, }: Props) { const setDataAttr = useCallback( (name: string, value: JsonPrimitive) => { onChange({ ...data, [name]: value === DYNAMIC_FORM_NULL_ARG ? undefined : value }); }, [data, onChange], ); return ( ); } function FormInputsStack>({ className, ...props }: FormInputsProps & { className?: string }) { return ( ); } type FormInputsProps = Pick< Props, "inputs" | "autocompleteFunctions" | "autocompleteVariables" | "stateKey" | "data" > & { setDataAttr: (name: string, value: JsonPrimitive) => void; disabled?: boolean; }; function FormInputs>({ inputs, autocompleteFunctions, autocompleteVariables, stateKey, setDataAttr, data, disabled, }: FormInputsProps) { return ( <> {inputs?.map((input, i) => { if ("hidden" in input && input.hidden) { return null; } if ("disabled" in input && disabled != null) { input.disabled = disabled; } switch (input.type) { case "select": return ( setDataAttr(input.name, v)} value={ data[input.name] ? String(data[input.name]) : (input.defaultValue ?? DYNAMIC_FORM_NULL_ARG) } /> ); case "text": return ( setDataAttr(input.name, v)} value={ data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? "") } /> ); case "editor": return ( setDataAttr(input.name, v)} value={ data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? "") } /> ); case "checkbox": return ( setDataAttr(input.name, v)} value={data[input.name] != null ? data[input.name] === true : false} /> ); case "http_request": return ( setDataAttr(input.name, v)} value={data[input.name] != null ? String(data[input.name]) : DYNAMIC_FORM_NULL_ARG} /> ); case "file": return ( setDataAttr(input.name, v)} filePath={ data[input.name] != null ? String(data[input.name]) : DYNAMIC_FORM_NULL_ARG } /> ); case "accordion": if (!hasVisibleInputs(input.inputs)) { return null; } return (
); case "h_stack": if (!hasVisibleInputs(input.inputs)) { return null; } return (
); case "banner": if (!hasVisibleInputs(input.inputs)) { return null; } return ( ); case "markdown": return {input.content}; case "key_value": return ( setDataAttr(input.name, v)} value={ data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? "[]") } /> ); default: // @ts-expect-error throw new Error(`Invalid input type: ${input.type}`); } })} ); } function TextArg({ arg, onChange, value, autocompleteFunctions, autocompleteVariables, stateKey, }: { arg: FormInputText; value: string; onChange: (v: string) => void; autocompleteFunctions: boolean; autocompleteVariables: boolean; stateKey: string; }) { const props: InputProps = { onChange, name: arg.name, multiLine: arg.multiLine, className: arg.multiLine ? "min-h-[4rem]" : undefined, defaultValue: value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value, required: !arg.optional, disabled: arg.disabled, help: arg.description, type: arg.password ? "password" : "text", label: arg.label ?? arg.name, size: INPUT_SIZE, hideLabel: arg.hideLabel ?? arg.label == null, placeholder: arg.placeholder ?? undefined, forceUpdateKey: stateKey, autocomplete: arg.completionOptions ? { options: arg.completionOptions } : undefined, stateKey, autocompleteFunctions, autocompleteVariables, }; if (autocompleteVariables || autocompleteFunctions || arg.completionOptions) { return ; } return ; } function EditorArg({ arg, onChange, value, autocompleteFunctions, autocompleteVariables, stateKey, }: { arg: FormInputEditor; value: string; onChange: (v: string) => void; autocompleteFunctions: boolean; autocompleteVariables: boolean; stateKey: string; }) { const id = `input-${arg.name}`; // Read-only editor force refresh for every defaultValue change // Should this be built into the component? const [popoutKey, regeneratePopoutKey] = useRandomKey(); const forceUpdateKey = popoutKey + (arg.readOnly ? arg.defaultValue + stateKey : stateKey); return (
{ showDialog({ id: "id", size: "full", title: arg.readOnly ? "View Value" : "Edit Value", className: "!max-w-[50rem] !max-h-[60rem]", description: arg.label && ( ), onClose() { // Force the main editor to update on close regeneratePopoutKey(); }, render() { return ( ); }, }); }} />
} />
); } function SelectArg({ arg, value, onChange, }: { arg: FormInputSelect; value: string; onChange: (v: string) => void; }) { return ( { return { label: buildRequestBreadcrumbs(r, folders).join(" / ") + (r.id === activeHttpRequest?.id ? " (current)" : ""), value: r.id, }; })} /> ); } function buildRequestBreadcrumbs(request: HttpRequest, folders: Folder[]): string[] { const ancestors: (HttpRequest | Folder)[] = [request]; const next = () => { const latest = ancestors[0]; if (latest == null) return []; const parent = folders.find((f) => f.id === latest.folderId); if (parent == null) return; ancestors.unshift(parent); next(); }; next(); return ancestors.map((a) => (a.model === "folder" ? a.name : resolvedModelName(a))); } function CheckboxArg({ arg, onChange, value, }: { arg: FormInputCheckbox; value: boolean; onChange: (v: boolean) => void; }) { return ( ); } function KeyValueArg({ arg, onChange, value, stateKey, }: { arg: FormInputKeyValue; value: string; onChange: (v: string) => void; stateKey: string; }) { const pairs: Pair[] = useMemo(() => { try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : []; } catch { return []; } }, [value]); const handleChange = useCallback( (newPairs: Pair[]) => { onChange(JSON.stringify(newPairs)); }, [onChange], ); return (
); } function hasVisibleInputs(inputs: FormInput[] | undefined): boolean { if (!inputs) return false; for (const input of inputs) { if ("inputs" in input && !hasVisibleInputs(input.inputs)) { // Has children, but none are visible return false; } if (!input.hidden) { return true; } } return false; }