mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-15 13:43:39 +01:00
315 lines
8.6 KiB
TypeScript
315 lines
8.6 KiB
TypeScript
import type { Folder, HttpRequest } from '@yaakapp-internal/models';
|
|
import type {
|
|
TemplateFunction,
|
|
TemplateFunctionArg,
|
|
TemplateFunctionCheckboxArg,
|
|
TemplateFunctionFileArg,
|
|
TemplateFunctionHttpRequestArg,
|
|
TemplateFunctionSelectArg,
|
|
TemplateFunctionTextArg,
|
|
} from '@yaakapp-internal/plugins';
|
|
import type { FnArg, Tokens } from '@yaakapp-internal/templates';
|
|
import classNames from 'classnames';
|
|
import { useCallback, useMemo, useState } from 'react';
|
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
|
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
import { useFolders } from '../hooks/useFolders';
|
|
import { useHttpRequests } from '../hooks/useHttpRequests';
|
|
import { useRenderTemplate } from '../hooks/useRenderTemplate';
|
|
import { useTemplateTokensToString } from '../hooks/useTemplateTokensToString';
|
|
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
|
import { Button } from './core/Button';
|
|
import { Checkbox } from './core/Checkbox';
|
|
import { InlineCode } from './core/InlineCode';
|
|
import { PlainInput } from './core/PlainInput';
|
|
import { Select } from './core/Select';
|
|
import { VStack } from './core/Stacks';
|
|
import { SelectFile } from './SelectFile';
|
|
|
|
const NULL_ARG = '__NULL__';
|
|
|
|
interface Props {
|
|
templateFunction: TemplateFunction;
|
|
initialTokens: Tokens;
|
|
hide: () => void;
|
|
onChange: (insert: string) => void;
|
|
}
|
|
|
|
export function TemplateFunctionDialog({ templateFunction, hide, initialTokens, onChange }: Props) {
|
|
const [argValues, setArgValues] = useState<Record<string, string | boolean>>(() => {
|
|
const initial: Record<string, string> = {};
|
|
const initialArgs =
|
|
initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'fn'
|
|
? initialTokens.tokens[0]?.val.args
|
|
: [];
|
|
for (const arg of templateFunction.args) {
|
|
const initialArg = initialArgs.find((a) => a.name === arg.name);
|
|
const initialArgValue =
|
|
initialArg?.value.type === 'str'
|
|
? initialArg?.value.text
|
|
: // TODO: Implement variable-based args
|
|
'__NULL__';
|
|
initial[arg.name] = initialArgValue ?? NULL_ARG;
|
|
}
|
|
|
|
return initial;
|
|
});
|
|
|
|
const setArgValue = useCallback((name: string, value: string | boolean | null) => {
|
|
setArgValues((v) => ({ ...v, [name]: value == null ? '__NULL__' : value }));
|
|
}, []);
|
|
|
|
const tokens: Tokens = useMemo(() => {
|
|
const argTokens: FnArg[] = Object.keys(argValues).map((name) => ({
|
|
name,
|
|
value:
|
|
argValues[name] === 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 ?? '').length > 10000;
|
|
|
|
return (
|
|
<VStack className="pb-3" space={4}>
|
|
<h1 className="font-mono !text-base">{templateFunction.name}(…)</h1>
|
|
<VStack space={2}>
|
|
{templateFunction.args.map((a: TemplateFunctionArg, i: number) => {
|
|
switch (a.type) {
|
|
case 'select':
|
|
return (
|
|
<SelectArg
|
|
key={i}
|
|
arg={a}
|
|
onChange={(v) => setArgValue(a.name, v)}
|
|
value={argValues[a.name] ? String(argValues[a.name]) : '__ERROR__'}
|
|
/>
|
|
);
|
|
case 'text':
|
|
return (
|
|
<TextArg
|
|
key={i}
|
|
arg={a}
|
|
onChange={(v) => setArgValue(a.name, v)}
|
|
value={argValues[a.name] ? String(argValues[a.name]) : '__ERROR__'}
|
|
/>
|
|
);
|
|
case 'checkbox':
|
|
return (
|
|
<CheckboxArg
|
|
key={i}
|
|
arg={a}
|
|
onChange={(v) => setArgValue(a.name, v)}
|
|
value={argValues[a.name] !== undefined ? argValues[a.name] === true : false}
|
|
/>
|
|
);
|
|
case 'http_request':
|
|
return (
|
|
<HttpRequestArg
|
|
key={i}
|
|
arg={a}
|
|
onChange={(v) => setArgValue(a.name, v)}
|
|
value={argValues[a.name] ? String(argValues[a.name]) : '__ERROR__'}
|
|
/>
|
|
);
|
|
case 'file':
|
|
return (
|
|
<FileArg
|
|
key={i}
|
|
arg={a}
|
|
onChange={(v) => setArgValue(a.name, v)}
|
|
filePath={argValues[a.name] ? String(argValues[a.name]) : '__ERROR__'}
|
|
/>
|
|
);
|
|
}
|
|
})}
|
|
</VStack>
|
|
<VStack className="w-full">
|
|
<div className="text-sm text-text-subtle">Preview</div>
|
|
<InlineCode
|
|
className={classNames(
|
|
'whitespace-pre select-text cursor-text max-h-[10rem] overflow-y-auto hide-scrollbars',
|
|
tooLarge && 'italic text-danger',
|
|
)}
|
|
>
|
|
{tooLarge ? 'too large to preview' : rendered.data || <> </>}
|
|
</InlineCode>
|
|
</VStack>
|
|
<Button color="primary" onClick={handleDone}>
|
|
Done
|
|
</Button>
|
|
</VStack>
|
|
);
|
|
}
|
|
|
|
function TextArg({
|
|
arg,
|
|
onChange,
|
|
value,
|
|
}: {
|
|
arg: TemplateFunctionTextArg;
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
}) {
|
|
const handleChange = useCallback(
|
|
(value: string) => {
|
|
onChange(value === '' ? NULL_ARG : value);
|
|
},
|
|
[onChange],
|
|
);
|
|
|
|
return (
|
|
<PlainInput
|
|
name={arg.name}
|
|
onChange={handleChange}
|
|
defaultValue={value === NULL_ARG ? '' : value}
|
|
require={!arg.optional}
|
|
label={
|
|
<>
|
|
{arg.label ?? arg.name}
|
|
{arg.optional && <span> (optional)</span>}
|
|
</>
|
|
}
|
|
hideLabel={arg.label == null}
|
|
placeholder={arg.placeholder ?? arg.defaultValue ?? ''}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function SelectArg({
|
|
arg,
|
|
value,
|
|
onChange,
|
|
}: {
|
|
arg: TemplateFunctionSelectArg;
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
}) {
|
|
return (
|
|
<Select
|
|
label={arg.label ?? arg.name}
|
|
name={arg.name}
|
|
onChange={onChange}
|
|
value={value}
|
|
options={[
|
|
...arg.options.map((a) => ({
|
|
label: a.label + (arg.defaultValue === a.value ? ' (default)' : ''),
|
|
value: a.value === arg.defaultValue ? NULL_ARG : a.value,
|
|
})),
|
|
]}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function FileArg({
|
|
arg,
|
|
filePath,
|
|
onChange,
|
|
}: {
|
|
arg: TemplateFunctionFileArg;
|
|
filePath: string;
|
|
onChange: (v: string | null) => void;
|
|
}) {
|
|
return (
|
|
<SelectFile
|
|
onChange={({ filePath }) => onChange(filePath)}
|
|
filePath={filePath === '__NULL__' ? null : filePath}
|
|
directory={!!arg.directory}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function HttpRequestArg({
|
|
arg,
|
|
value,
|
|
onChange,
|
|
}: {
|
|
arg: TemplateFunctionHttpRequestArg;
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
}) {
|
|
const folders = useFolders();
|
|
const httpRequests = useHttpRequests();
|
|
const activeRequest = useActiveRequest();
|
|
return (
|
|
<Select
|
|
label={arg.label ?? arg.name}
|
|
name={arg.name}
|
|
onChange={onChange}
|
|
value={value}
|
|
options={[
|
|
...httpRequests.map((r) => {
|
|
return {
|
|
label: buildRequestBreadcrumbs(r, folders).join(' / ') + (r.id == activeRequest?.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 : fallbackRequestName(a)));
|
|
}
|
|
|
|
function CheckboxArg({
|
|
arg,
|
|
onChange,
|
|
value,
|
|
}: {
|
|
arg: TemplateFunctionCheckboxArg;
|
|
value: boolean;
|
|
onChange: (v: boolean) => void;
|
|
}) {
|
|
return (
|
|
<Checkbox
|
|
onChange={onChange}
|
|
checked={value}
|
|
title={arg.label ?? arg.name}
|
|
hideLabel={arg.label == null}
|
|
/>
|
|
);
|
|
}
|