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': return (
); case 'h_stack': return (
); case 'banner': 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) { 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 (
); }