import type { EditorView } from '@codemirror/view'; 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 { parseTemplate } from '@yaakapp-internal/templates'; import classNames from 'classnames'; import { useEffect, useMemo, useState } from 'react'; import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace'; 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 { showDialog } from '../lib/dialog'; import { convertTemplateToInsecure } from '../lib/encryption'; import { jotaiStore } from '../lib/jotai'; import { setupOrConfigureEncryption } from '../lib/setupOrConfigureEncryption'; import { Button } from './core/Button'; import { collectArgumentValues } from './core/Editor/twig/util'; import { IconButton } from './core/IconButton'; import { InlineCode } from './core/InlineCode'; import { LoadingIcon } from './core/LoadingIcon'; import { PlainInput } from './core/PlainInput'; import { HStack } 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 () => { const initial = collectArgumentValues(initialTokens, templateFunction); // 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, templateFunction.args, templateFunction.name, ]); if (initialArgValues == null) return null; return ( ); } function InitializedTemplateFunctionDialog({ templateFunction: { name, previewType: ogPreviewType }, initialArgValues, hide, onChange, model, }: Omit & { initialArgValues: Record; }) { const previewType = ogPreviewType == null ? 'live' : ogPreviewType; 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); const handleDone = () => { if (tagText.data) { onChange(tagText.data); } hide(); }; const debouncedTagText = useDebouncedValue(tagText.data ?? '', 400); const [renderKey, setRenderKey] = useState(null); const rendered = useRenderTemplate({ template: debouncedTagText, enabled: previewType !== 'none', purpose: previewType === 'click' ? 'send' : 'preview', refreshKey: previewType === 'live' ? renderKey + debouncedTagText : renderKey, ignoreError: false, }); const tooLarge = rendered.data ? rendered.data.length > 10000 : false; // biome-ignore lint/correctness/useExhaustiveDependencies: Only update this on rendered data change to keep secrets hidden on input change const dataContainsSecrets = useMemo(() => { for (const [name, value] of Object.entries(argValues)) { const arg = templateFunction.data?.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; }, [rendered.data]); if (templateFunction.data == null || templateFunction.isPending) { return (
); } return (
{ e.preventDefault(); handleDone(); }} >
{name === 'secure' ? ( setArgValues({ ...argValues, value })} /> ) : ( )}
{previewType !== 'none' ? (
Rendered Preview {rendered.isLoading && }
{rendered.error || tagText.error ? ( {`${rendered.error || tagText.error}`.replace(/^Render Error: /, '')} ) : dataContainsSecrets && !showSecretsInPreview ? ( ------ sensitive values hidden ------ ) : tooLarge ? ( 'too large to preview' ) : ( rendered.data || <>  )}
{ setRenderKey(new Date().toISOString()); }} />
) : ( )}
{templateFunction.data.name === 'secure' && ( )}
); } TemplateFunctionDialog.show = ( fn: TemplateFunction, tagValue: string, startPos: number, view: EditorView, ) => { const initialTokens = parseTemplate(tagValue); showDialog({ id: `template-function-${Math.random()}`, // Allow multiple at once size: 'md', className: 'h-[60rem]', noPadding: true, title: {fn.name}(…), description: fn.description, render: ({ hide }) => { const model = jotaiStore.get(activeWorkspaceAtom); if (model == null) return null; return ( { view.dispatch({ changes: [{ from: startPos, to: startPos + tagValue.length, insert }], }); }} /> ); }, }); };