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; // oxlint-disable-next-line react-hooks/exhaustive-deps -- 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 }], }); }} /> ); }, }); };