mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-11 17:46:41 -05:00
Improved prompt function add add ctx.* functions (#301)
This commit is contained in:
@@ -30,7 +30,6 @@ export const createFolder = createFastMutation<
|
||||
label: 'Name',
|
||||
defaultValue: 'Folder',
|
||||
title: 'New Folder',
|
||||
required: true,
|
||||
confirmText: 'Create',
|
||||
placeholder: 'Name',
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ import { IconButton } from './core/IconButton';
|
||||
import { Input } from './core/Input';
|
||||
import { Label } from './core/Label';
|
||||
import { Select } from './core/Select';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { Markdown } from './Markdown';
|
||||
import { SelectFile } from './SelectFile';
|
||||
|
||||
@@ -216,7 +216,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
);
|
||||
case 'h_stack':
|
||||
return (
|
||||
<HStack key={i + stateKey} alignItems="end" space={3}>
|
||||
<div className="flex flex-wrap sm:flex-nowrap gap-3 items-end" key={i + stateKey}>
|
||||
<FormInputs
|
||||
data={data}
|
||||
disabled={disabled}
|
||||
@@ -226,7 +226,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
autocompleteFunctions={autocompleteFunctions || false}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
case 'banner':
|
||||
return (
|
||||
|
||||
@@ -50,7 +50,6 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
|
||||
const newMethod = await showPrompt({
|
||||
id: 'custom-method',
|
||||
label: 'Http Method',
|
||||
defaultValue: '',
|
||||
title: 'Custom Method',
|
||||
confirmText: 'Save',
|
||||
description: 'Enter a custom method name',
|
||||
|
||||
@@ -54,7 +54,6 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
|
||||
initial.value = await convertTemplateToInsecure(template);
|
||||
}
|
||||
|
||||
console.log('INITIAL', initial);
|
||||
setInitialArgValues(initial);
|
||||
})().catch(console.error);
|
||||
}, [
|
||||
@@ -78,7 +77,7 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
|
||||
}
|
||||
|
||||
function InitializedTemplateFunctionDialog({
|
||||
templateFunction: { name },
|
||||
templateFunction: { name, previewType: ogPreviewType },
|
||||
initialArgValues,
|
||||
hide,
|
||||
onChange,
|
||||
@@ -86,7 +85,7 @@ function InitializedTemplateFunctionDialog({
|
||||
}: Omit<Props, 'initialTokens'> & {
|
||||
initialArgValues: Record<string, string | boolean>;
|
||||
}) {
|
||||
const enablePreview = name !== 'secure';
|
||||
const previewType = ogPreviewType == null ? 'live' : ogPreviewType;
|
||||
const [showSecretsInPreview, toggleShowSecretsInPreview] = useToggle(false);
|
||||
const [argValues, setArgValues] = useState<Record<string, string | boolean>>(initialArgValues);
|
||||
|
||||
@@ -126,7 +125,14 @@ function InitializedTemplateFunctionDialog({
|
||||
};
|
||||
|
||||
const debouncedTagText = useDebouncedValue(tagText.data ?? '', 400);
|
||||
const rendered = useRenderTemplate(debouncedTagText);
|
||||
const [renderKey, setRenderKey] = useState<string | null>(null);
|
||||
const rendered = useRenderTemplate(
|
||||
debouncedTagText,
|
||||
previewType !== 'none',
|
||||
previewType === 'click' ? 'send' : 'preview',
|
||||
previewType === 'live' ? renderKey + debouncedTagText : renderKey,
|
||||
);
|
||||
|
||||
const tooLarge = rendered.data ? rendered.data.length > 10000 : false;
|
||||
const dataContainsSecrets = useMemo(() => {
|
||||
for (const [name, value] of Object.entries(argValues)) {
|
||||
@@ -174,12 +180,12 @@ function InitializedTemplateFunctionDialog({
|
||||
)}
|
||||
</div>
|
||||
<div className="px-6 border-t border-t-border py-3 bg-surface-highlight w-full flex flex-col gap-4">
|
||||
{enablePreview ? (
|
||||
{previewType !== 'none' ? (
|
||||
<VStack className="w-full">
|
||||
<HStack space={0.5}>
|
||||
<HStack className="text-sm text-text-subtle" space={1.5}>
|
||||
Rendered Preview
|
||||
{rendered.isPending && <LoadingIcon size="xs" />}
|
||||
{rendered.isLoading && <LoadingIcon size="xs" />}
|
||||
</HStack>
|
||||
<IconButton
|
||||
size="xs"
|
||||
@@ -195,6 +201,7 @@ function InitializedTemplateFunctionDialog({
|
||||
</HStack>
|
||||
<InlineCode
|
||||
className={classNames(
|
||||
'relative',
|
||||
'whitespace-pre-wrap !select-text cursor-text max-h-[10rem] overflow-y-auto hide-scrollbars !border-text-subtlest',
|
||||
tooLarge && 'italic text-danger',
|
||||
)}
|
||||
@@ -212,6 +219,18 @@ function InitializedTemplateFunctionDialog({
|
||||
) : (
|
||||
rendered.data || <> </>
|
||||
)}
|
||||
<div className="absolute right-0 top-0 bottom-0 flex items-center">
|
||||
<IconButton
|
||||
size="xs"
|
||||
icon="refresh"
|
||||
className="text-text-subtle"
|
||||
title="Refresh preview"
|
||||
spin={rendered.isLoading}
|
||||
onClick={() => {
|
||||
setRenderKey(new Date().toISOString());
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</InlineCode>
|
||||
</VStack>
|
||||
) : (
|
||||
@@ -250,8 +269,16 @@ function collectArgumentValues(initialTokens: Tokens, templateFunction: Template
|
||||
if (!('name' in arg)) return;
|
||||
|
||||
const initialArg = initialArgs.find((a) => a.name === arg.name);
|
||||
const initialArgValue = initialArg?.value.type === 'str' ? initialArg?.value.text : undefined;
|
||||
initial[arg.name] = initialArgValue ?? arg.defaultValue ?? DYNAMIC_FORM_NULL_ARG;
|
||||
const initialArgValue =
|
||||
initialArg?.value.type === 'str'
|
||||
? initialArg?.value.text
|
||||
: initialArg?.value.type === 'bool'
|
||||
? initialArg.value.value
|
||||
: undefined;
|
||||
const value = initialArgValue ?? arg.defaultValue;
|
||||
if (value != null) {
|
||||
initial[arg.name] = value;
|
||||
}
|
||||
};
|
||||
|
||||
templateFunction.args.forEach(processArg);
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { Tokens } from '@yaakapp-internal/templates';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useActiveEnvironmentVariables } from '../hooks/useActiveEnvironmentVariables';
|
||||
import { useRenderTemplate } from '../hooks/useRenderTemplate';
|
||||
import { useTemplateTokensToString } from '../hooks/useTemplateTokensToString';
|
||||
import { Button } from './core/Button';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { Select } from './core/Select';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
initialTokens: Tokens;
|
||||
hide: () => void;
|
||||
onChange: (rawTag: string) => void;
|
||||
}
|
||||
|
||||
export function TemplateVariableDialog({ hide, onChange, initialTokens }: Props) {
|
||||
const variables = useActiveEnvironmentVariables();
|
||||
const [selectedVariableName, setSelectedVariableName] = useState<string>(() => {
|
||||
return initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'var'
|
||||
? initialTokens.tokens[0]?.val.name
|
||||
: ''; // Should never happen
|
||||
});
|
||||
|
||||
const tokens: Tokens = useMemo(() => {
|
||||
const selectedVariable = variables.find((v) => v.name === selectedVariableName);
|
||||
return {
|
||||
tokens: [
|
||||
{
|
||||
type: 'tag',
|
||||
val: {
|
||||
type: 'var',
|
||||
name: selectedVariable?.name ?? '',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [selectedVariableName, variables]);
|
||||
|
||||
const tagText = useTemplateTokensToString(tokens);
|
||||
|
||||
const handleDone = useCallback(async () => {
|
||||
if (tagText.data != null) {
|
||||
onChange(tagText.data);
|
||||
}
|
||||
hide();
|
||||
}, [hide, onChange, tagText.data]);
|
||||
|
||||
const rendered = useRenderTemplate(tagText.data ?? '');
|
||||
|
||||
return (
|
||||
<VStack className="pb-3" space={4}>
|
||||
<VStack space={2}>
|
||||
<Select
|
||||
name="variable"
|
||||
label="Select Variable"
|
||||
value={selectedVariableName}
|
||||
options={variables.map((v) => ({ label: v.name, value: v.name }))}
|
||||
onChange={setSelectedVariableName}
|
||||
/>
|
||||
</VStack>
|
||||
<VStack>
|
||||
<div className="text-sm text-text-subtle">Preview</div>
|
||||
<InlineCode className="select-text cursor-text">{rendered.data}</InlineCode>
|
||||
</VStack>
|
||||
<Button color="primary" onClick={handleDone}>
|
||||
Done
|
||||
</Button>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export interface BannerProps {
|
||||
|
||||
export function Banner({ children, className, color }: BannerProps) {
|
||||
return (
|
||||
<div className="w-full mb-auto grid grid-rows-1 max-h-full">
|
||||
<div className="w-auto grid grid-rows-1 max-h-full flex-0">
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
@@ -18,6 +18,7 @@ export function Banner({ children, className, color }: BannerProps) {
|
||||
'border border-border border-dashed',
|
||||
'px-4 py-2 rounded-lg select-auto',
|
||||
'overflow-auto text-text',
|
||||
'mb-auto', // Don't stretch all the way down if the parent is in grid or flexbox
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -723,6 +723,7 @@ function FileActionsDropdown({
|
||||
id: 'content-type',
|
||||
title: 'Override Content-Type',
|
||||
label: 'Content-Type',
|
||||
required: false,
|
||||
placeholder: 'text/plain',
|
||||
defaultValue: pair.contentType ?? '',
|
||||
confirmText: 'Set',
|
||||
|
||||
@@ -16,6 +16,7 @@ export function Prompt({
|
||||
label,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
password,
|
||||
onResult,
|
||||
required,
|
||||
confirmText,
|
||||
@@ -36,10 +37,10 @@ export function Prompt({
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<PlainInput
|
||||
hideLabel
|
||||
autoSelect
|
||||
required={required}
|
||||
placeholder={placeholder ?? 'Enter text'}
|
||||
type={password ? 'password' : 'text'}
|
||||
label={label}
|
||||
defaultValue={defaultValue}
|
||||
onChange={setValue}
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { RenderPurpose } from '@yaakapp-internal/plugins';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { minPromiseMillis } from '../lib/minPromiseMillis';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
|
||||
|
||||
export function useRenderTemplate(template: string) {
|
||||
export function useRenderTemplate(
|
||||
template: string,
|
||||
enabled: boolean,
|
||||
purpose: RenderPurpose,
|
||||
refreshKey: string | null,
|
||||
) {
|
||||
const workspaceId = useAtomValue(activeWorkspaceIdAtom) ?? 'n/a';
|
||||
const environmentId = useActiveEnvironment()?.id ?? null;
|
||||
return useQuery<string>({
|
||||
refetchOnWindowFocus: false,
|
||||
queryKey: ['render_template', template, workspaceId, environmentId],
|
||||
queryFn: () => minPromiseMillis(renderTemplate({ template, workspaceId, environmentId }), 200),
|
||||
enabled,
|
||||
queryKey: ['render_template', workspaceId, environmentId, refreshKey, purpose],
|
||||
queryFn: () =>
|
||||
minPromiseMillis(renderTemplate({ template, workspaceId, environmentId, purpose }), 300),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,12 +27,14 @@ export async function renderTemplate({
|
||||
template,
|
||||
workspaceId,
|
||||
environmentId,
|
||||
purpose,
|
||||
}: {
|
||||
template: string;
|
||||
workspaceId: string;
|
||||
environmentId: string | null;
|
||||
purpose: RenderPurpose;
|
||||
}): Promise<string> {
|
||||
return invokeCmd('cmd_render_template', { template, workspaceId, environmentId });
|
||||
return invokeCmd('cmd_render_template', { template, workspaceId, environmentId, purpose });
|
||||
}
|
||||
|
||||
export async function decryptTemplate({
|
||||
|
||||
@@ -6,7 +6,13 @@ import { showDialog } from './dialog';
|
||||
type PromptArgs = Pick<DialogProps, 'title' | 'description'> &
|
||||
Omit<PromptProps, 'onClose' | 'onCancel' | 'onResult'> & { id: string };
|
||||
|
||||
export async function showPrompt({ id, title, description, ...props }: PromptArgs) {
|
||||
export async function showPrompt({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
required = true,
|
||||
...props
|
||||
}: PromptArgs) {
|
||||
return new Promise((resolve: PromptProps['onResult']) => {
|
||||
showDialog({
|
||||
id,
|
||||
@@ -21,6 +27,7 @@ export async function showPrompt({ id, title, description, ...props }: PromptArg
|
||||
},
|
||||
render: ({ hide }) =>
|
||||
Prompt({
|
||||
required,
|
||||
onCancel: () => {
|
||||
// Click cancel button within dialog
|
||||
resolve(null);
|
||||
|
||||
@@ -11,6 +11,7 @@ export async function renameModelWithPrompt(model: Extract<AnyModel, { name: str
|
||||
const name = await showPrompt({
|
||||
id: 'rename-request',
|
||||
title: 'Rename Request',
|
||||
required: false,
|
||||
description:
|
||||
model.name === '' ? (
|
||||
'Enter a new name'
|
||||
|
||||
@@ -20,7 +20,13 @@ export function setWorkspaceSearchParams(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
search: (prev: any) => {
|
||||
// console.log('Navigating to', { prev, search });
|
||||
return { ...prev, ...search };
|
||||
const o = { ...prev, ...search };
|
||||
for (const k of Object.keys(o)) {
|
||||
if (o[k] == null) {
|
||||
delete o[k];
|
||||
}
|
||||
}
|
||||
return o;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user