import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace, } from '@yaakapp-internal/models'; import type { TemplateFunction } from '@yaakapp-internal/plugins'; import type { FnArg, Tokens } from '@yaakapp-internal/templates'; import classNames from 'classnames'; import { useEffect, useMemo, useState } from 'react'; import { useDebouncedValue } from '../hooks/useDebouncedValue'; import { useRenderTemplate } from '../hooks/useRenderTemplate'; import { useTemplateFunctionConfig } from '../hooks/useTemplateFunctionConfig'; import { templateTokensToString, useTemplateTokensToString, } from '../hooks/useTemplateTokensToString'; import { useToggle } from '../hooks/useToggle'; import { convertTemplateToInsecure } from '../lib/encryption'; import { setupOrConfigureEncryption } from '../lib/setupOrConfigureEncryption'; import { Button } from './core/Button'; import { IconButton } from './core/IconButton'; import { InlineCode } from './core/InlineCode'; import { LoadingIcon } from './core/LoadingIcon'; import { PlainInput } from './core/PlainInput'; import { HStack, VStack } from './core/Stacks'; import { DYNAMIC_FORM_NULL_ARG, DynamicForm } from './DynamicForm'; interface Props { templateFunction: TemplateFunction; initialTokens: Tokens; hide: () => void; onChange: (insert: string) => void; model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace; } export function TemplateFunctionDialog({ initialTokens, templateFunction, ...props }: Props) { const [initialArgValues, setInitialArgValues] = useState | null>( null, ); useEffect(() => { if (initialArgValues != null) { return; } (async function () { 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; } // HACK: Replace the secure() function's encrypted `value` arg with the decrypted version so // we can display it in the editor input. if (templateFunction.name === 'secure') { const template = await templateTokensToString(initialTokens); initial.value = await convertTemplateToInsecure(template); } setInitialArgValues(initial); })().catch(console.error); }, [ initialArgValues, initialTokens, initialTokens.tokens, templateFunction.args, templateFunction.name, ]); if (initialArgValues == null) return null; return ( ); } function InitializedTemplateFunctionDialog({ templateFunction: { name }, initialArgValues, hide, onChange, model, }: Omit & { initialArgValues: Record; }) { const enablePreview = name !== 'secure'; const [showSecretsInPreview, toggleShowSecretsInPreview] = useToggle(false); const [argValues, setArgValues] = useState>(initialArgValues); 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, args: argTokens, }, }, ], }; }, [argValues, name]); const tagText = useTemplateTokensToString(tokens); const templateFunction = useTemplateFunctionConfig(name, argValues, model).data; const handleDone = () => { if (tagText.data) { onChange(tagText.data); } hide(); }; const debouncedTagText = useDebouncedValue(tagText.data ?? '', 400); 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 arg = templateFunction?.args.find((a) => 'name' in a && a.name === name); const isTextPassword = arg?.type === 'text' && arg.password; if (isTextPassword && 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]); if (templateFunction == null) return null; return ( { e.preventDefault(); handleDone(); }} > {name === 'secure' ? ( setArgValues({ ...argValues, value })} /> ) : ( )} {enablePreview && ( Rendered Preview {rendered.isPending && } {rendered.error || tagText.error ? ( {`${rendered.error || tagText.error}`.replace(/^Render Error: /, '')} ) : dataContainsSecrets && !showSecretsInPreview ? ( ------ sensitive values hidden ------ ) : tooLarge ? ( 'too large to preview' ) : ( rendered.data || <>  )} )}
{templateFunction.name === 'secure' && ( )}
); }