import type { TemplateFunction } from '@yaakapp-internal/plugins'; import type { FnArg, Tokens } from '@yaakapp-internal/templates'; import classNames from 'classnames'; import { useMemo, useState } from 'react'; import { useDebouncedValue } from '../hooks/useDebouncedValue'; import { useRenderTemplate } from '../hooks/useRenderTemplate'; import { useTemplateTokensToString } from '../hooks/useTemplateTokensToString'; import { useToggle } from '../hooks/useToggle'; import { Button } from './core/Button'; import { InlineCode } from './core/InlineCode'; import { HStack, VStack } from './core/Stacks'; import { DYNAMIC_FORM_NULL_ARG, DynamicForm } from './DynamicForm'; import { IconButton } from './core/IconButton'; import { Banner } from './core/Banner'; interface Props { templateFunction: TemplateFunction; initialTokens: Tokens; hide: () => void; onChange: (insert: string) => void; } export function TemplateFunctionDialog({ templateFunction, hide, initialTokens, onChange }: Props) { const [showSecretsInPreview, toggleShowSecretsInPreview] = useToggle(false); const [argValues, setArgValues] = useState>(() => { const initial: Record = {}; const initialArgs = initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'fn' ? initialTokens.tokens[0]?.val.args : []; for (const arg of templateFunction.args) { if (!('name' in arg)) { // Skip visual-only args continue; } const initialArg = initialArgs.find((a) => a.name === arg.name); const initialArgValue = initialArg?.value.type === 'str' ? initialArg?.value.text : // TODO: Implement variable-based args undefined; initial[arg.name] = initialArgValue ?? arg.defaultValue ?? DYNAMIC_FORM_NULL_ARG; } return initial; }); const tokens: Tokens = useMemo(() => { const argTokens: FnArg[] = Object.keys(argValues).map((name) => ({ name, value: argValues[name] === DYNAMIC_FORM_NULL_ARG ? { type: 'null' } : typeof argValues[name] === 'boolean' ? { type: 'bool', value: argValues[name] === true } : { type: 'str', text: String(argValues[name] ?? '') }, })); return { tokens: [ { type: 'tag', val: { type: 'fn', name: templateFunction.name, args: argTokens, }, }, ], }; }, [argValues, templateFunction.name]); const tagText = useTemplateTokensToString(tokens); const handleDone = () => { if (tagText.data) { onChange(tagText.data); } hide(); }; const debouncedTagText = useDebouncedValue(tagText.data ?? '', 200); const rendered = useRenderTemplate(debouncedTagText); const tooLarge = rendered.data ? rendered.data.length > 10000 : false; const dataContainsSecrets = useMemo(() => { for (const [name, value] of Object.entries(argValues)) { const isPassword = templateFunction.args.some( (a) => a.type === 'text' && a.password && a.name === name, ); if (isPassword && typeof value === 'string' && value && rendered.data?.includes(value)) { return true; } } return false; // Only update this on rendered data change to keep secrets hidden on input change // eslint-disable-next-line react-hooks/exhaustive-deps }, [rendered.data]); return (

{templateFunction.name}(…)

Rendered Preview
{rendered.error || tagText.error ? ( {`${rendered.error || tagText.error}`} ) : ( {dataContainsSecrets && !showSecretsInPreview ? ( ------ sensitive values hidden ------ ) : tooLarge ? ( 'too large to preview' ) : ( rendered.data || <>  )} )}
); }