Improved prompt function add add ctx.* functions (#301)

This commit is contained in:
Gregory Schier
2025-11-15 08:19:58 -08:00
committed by GitHub
parent 7ced183b11
commit 84219571e8
29 changed files with 454 additions and 150 deletions

21
package-lock.json generated
View File

@@ -30,6 +30,7 @@
"plugins/importer-postman-environment",
"plugins/importer-yaak",
"plugins/template-function-cookie",
"plugins/template-function-ctx",
"plugins/template-function-encode",
"plugins/template-function-fs",
"plugins/template-function-hash",
@@ -4344,6 +4345,10 @@
"resolved": "plugins/template-function-cookie",
"link": true
},
"node_modules/@yaak/template-function-ctx": {
"resolved": "plugins/template-function-ctx",
"link": true
},
"node_modules/@yaak/template-function-encode": {
"resolved": "plugins/template-function-encode",
"link": true
@@ -18969,7 +18974,7 @@
},
"packages/plugin-runtime-types": {
"name": "@yaakapp/api",
"version": "0.7.0",
"version": "0.7.1",
"dependencies": {
"@types/node": "^24.0.13"
},
@@ -19097,6 +19102,10 @@
"name": "@yaak/template-function-cookie",
"version": "0.1.0"
},
"plugins/template-function-ctx": {
"name": "@yaak/template-function-ctx",
"version": "0.1.0"
},
"plugins/template-function-datetime": {
"version": "0.1.0",
"extraneous": true,
@@ -19128,7 +19137,10 @@
},
"plugins/template-function-prompt": {
"name": "@yaak/template-function-prompt",
"version": "0.1.0"
"version": "0.1.0",
"dependencies": {
"slugify": "^1.6.6"
}
},
"plugins/template-function-random": {
"name": "@yaak/template-function-random",
@@ -19177,6 +19189,11 @@
"uuid": "dist/esm/bin/uuid"
}
},
"plugins/template-function-window": {
"name": "@yaak/template-function-window",
"version": "0.1.0",
"extraneous": true
},
"plugins/template-function-xml": {
"name": "@yaak/template-function-xml",
"version": "0.1.0",

View File

@@ -29,6 +29,7 @@
"plugins/importer-postman-environment",
"plugins/importer-yaak",
"plugins/template-function-cookie",
"plugins/template-function-ctx",
"plugins/template-function-encode",
"plugins/template-function-fs",
"plugins/template-function-hash",

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.7.0",
"version": "0.7.1",
"keywords": [
"api-client",
"insomnia-alternative",

View File

@@ -224,7 +224,7 @@ defaultValue?: string, disabled?: boolean,
*/
description?: string, };
export type FormInputHStack = { inputs?: Array<FormInput>, };
export type FormInputHStack = { inputs?: Array<FormInput>, hidden?: boolean, };
export type FormInputHttpRequest = {
/**
@@ -391,7 +391,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, context: PluginContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "window_info_request" } & WindowInfoRequest | { "type": "window_info_response" } & WindowInfoResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;
@@ -411,7 +411,7 @@ export type PromptTextRequest = { id: string, title: string, label: string, desc
/**
* Text to add to the confirmation button
*/
confirmText?: string,
confirmText?: string, password?: boolean,
/**
* Text to add to the cancel button
*/
@@ -445,7 +445,7 @@ export type SetKeyValueResponse = {};
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, timeout?: number, };
export type TemplateFunction = { name: string, description?: string,
export type TemplateFunction = { name: string, previewType?: TemplateFunctionPreviewType, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
@@ -457,6 +457,8 @@ aliases?: Array<string>, args: Array<TemplateFunctionArg>, };
*/
export type TemplateFunctionArg = FormInput;
export type TemplateFunctionPreviewType = "live" | "click" | "none";
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
@@ -487,6 +489,10 @@ export type ThemeComponentColors = { surface?: string, surfaceHighlight?: string
export type ThemeComponents = { dialog?: ThemeComponentColors, menu?: ThemeComponentColors, toast?: ThemeComponentColors, sidebar?: ThemeComponentColors, responsePane?: ThemeComponentColors, appHeader?: ThemeComponentColors, button?: ThemeComponentColors, banner?: ThemeComponentColors, templateTag?: ThemeComponentColors, urlBar?: ThemeComponentColors, editor?: ThemeComponentColors, input?: ThemeComponentColors, };
export type WindowInfoRequest = { label: string, };
export type WindowInfoResponse = { requestId: string | null, environmentId: string | null, workspaceId: string | null, label: string, };
export type WindowNavigateEvent = { url: string, };
export type WindowSize = { width: number, height: number, };

View File

@@ -18,7 +18,7 @@ import type {
ShowToastRequest,
TemplateRenderRequest,
} from '../bindings/gen_events.ts';
import { JsonValue } from '../bindings/serde_json/JsonValue';
import type { JsonValue } from '../bindings/serde_json/JsonValue';
export interface Context {
clipboard: {
@@ -36,6 +36,9 @@ export interface Context {
delete(key: string): Promise<boolean>;
};
window: {
requestId(): Promise<string | null>;
workspaceId(): Promise<string | null>;
environmentId(): Promise<string | null>;
openUrl(
args: OpenWindowRequest & {
onNavigate?: (args: { url: string }) => void;

View File

@@ -19,6 +19,7 @@ import {
SendHttpRequestResponse,
TemplateFunction,
TemplateRenderResponse,
WindowInfoResponse,
} from '@yaakapp-internal/plugins';
import { Context, PluginDefinition } from '@yaakapp/api';
import console from 'node:console';
@@ -317,7 +318,21 @@ export class PluginInstance {
Array.isArray(this.#mod?.templateFunctions)
) {
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
if (typeof fn?.onRender === 'function') {
if (
payload.args.purpose === 'preview' &&
(fn?.previewType === 'click' || fn?.previewType === 'none')
) {
// Send empty render response
this.#sendPayload(
context,
{
type: 'call_template_function_response',
value: null,
error: 'Live preview disabled for this function',
},
replyId,
);
} else if (typeof fn?.onRender === 'function') {
const resolvedArgs = await applyDynamicFormInput(ctx, fn.args, payload.args);
payload.args.values = applyFormInputDefaults(resolvedArgs, payload.args.values);
try {
@@ -410,7 +425,7 @@ export class PluginInstance {
return this.#sendPayload(context, { type: 'empty_response' }, replyId);
}
#sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
#sendForReply<T extends Omit<InternalEventPayload, 'type'>>(
context: PluginContext,
payload: InternalEventPayload,
): Promise<T> {
@@ -456,10 +471,22 @@ export class PluginInstance {
}
#newCtx(context: PluginContext): Context {
const _windowInfo = async () => {
if (context.label == null) {
throw new Error("Can't get window context without an active window");
}
const payload: InternalEventPayload = {
type: 'window_info_request',
label: context.label,
};
return this.#sendForReply<WindowInfoResponse>(context, payload);
};
return {
clipboard: {
copyText: async (text) => {
await this.#sendAndWaitForReply(context, {
await this.#sendForReply(context, {
type: 'copy_text_request',
text,
});
@@ -467,7 +494,7 @@ export class PluginInstance {
},
toast: {
show: async (args) => {
await this.#sendAndWaitForReply(context, {
await this.#sendForReply(context, {
type: 'show_toast_request',
// Handle default here because null/undefined both convert to None in Rust translation
timeout: args.timeout === undefined ? 5000 : args.timeout,
@@ -476,6 +503,15 @@ export class PluginInstance {
},
},
window: {
requestId: async () => {
return (await _windowInfo()).requestId;
},
async workspaceId(): Promise<string | null> {
return (await _windowInfo()).workspaceId;
},
async environmentId(): Promise<string | null> {
return (await _windowInfo()).environmentId;
},
openUrl: async ({ onNavigate, onClose, ...args }) => {
args.label = args.label || `${Math.random()}`;
const payload: InternalEventPayload = { type: 'open_window_request', ...args };
@@ -500,7 +536,7 @@ export class PluginInstance {
},
prompt: {
text: async (args) => {
const reply: PromptTextResponse = await this.#sendAndWaitForReply(context, {
const reply: PromptTextResponse = await this.#sendForReply(context, {
type: 'prompt_text_request',
...args,
});
@@ -513,7 +549,7 @@ export class PluginInstance {
type: 'find_http_responses_request',
...args,
} as const;
const { httpResponses } = await this.#sendAndWaitForReply<FindHttpResponsesResponse>(
const { httpResponses } = await this.#sendForReply<FindHttpResponsesResponse>(
context,
payload,
);
@@ -526,7 +562,7 @@ export class PluginInstance {
type: 'render_grpc_request_request',
...args,
} as const;
const { grpcRequest } = await this.#sendAndWaitForReply<RenderGrpcRequestResponse>(
const { grpcRequest } = await this.#sendForReply<RenderGrpcRequestResponse>(
context,
payload,
);
@@ -539,7 +575,7 @@ export class PluginInstance {
type: 'get_http_request_by_id_request',
...args,
} as const;
const { httpRequest } = await this.#sendAndWaitForReply<GetHttpRequestByIdResponse>(
const { httpRequest } = await this.#sendForReply<GetHttpRequestByIdResponse>(
context,
payload,
);
@@ -550,7 +586,7 @@ export class PluginInstance {
type: 'send_http_request_request',
...args,
} as const;
const { httpResponse } = await this.#sendAndWaitForReply<SendHttpRequestResponse>(
const { httpResponse } = await this.#sendForReply<SendHttpRequestResponse>(
context,
payload,
);
@@ -561,7 +597,7 @@ export class PluginInstance {
type: 'render_http_request_request',
...args,
} as const;
const { httpRequest } = await this.#sendAndWaitForReply<RenderHttpRequestResponse>(
const { httpRequest } = await this.#sendForReply<RenderHttpRequestResponse>(
context,
payload,
);
@@ -574,18 +610,12 @@ export class PluginInstance {
type: 'get_cookie_value_request',
...args,
} as const;
const { value } = await this.#sendAndWaitForReply<GetCookieValueResponse>(
context,
payload,
);
const { value } = await this.#sendForReply<GetCookieValueResponse>(context, payload);
return value;
},
listNames: async () => {
const payload = { type: 'list_cookie_names_request' } as const;
const { names } = await this.#sendAndWaitForReply<ListCookieNamesResponse>(
context,
payload,
);
const { names } = await this.#sendForReply<ListCookieNamesResponse>(context, payload);
return names;
},
},
@@ -596,14 +626,14 @@ export class PluginInstance {
*/
render: async (args) => {
const payload = { type: 'template_render_request', ...args } as const;
const result = await this.#sendAndWaitForReply<TemplateRenderResponse>(context, payload);
const result = await this.#sendForReply<TemplateRenderResponse>(context, payload);
return result.data as any;
},
},
store: {
get: async <T>(key: string) => {
const payload = { type: 'get_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<GetKeyValueResponse>(context, payload);
const result = await this.#sendForReply<GetKeyValueResponse>(context, payload);
return result.value ? (JSON.parse(result.value) as T) : undefined;
},
set: async <T>(key: string, value: T) => {
@@ -613,11 +643,11 @@ export class PluginInstance {
key,
value: valueStr,
};
await this.#sendAndWaitForReply<GetKeyValueResponse>(context, payload);
await this.#sendForReply<GetKeyValueResponse>(context, payload);
},
delete: async (key: string) => {
const payload = { type: 'delete_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<DeleteKeyValueResponse>(context, payload);
const result = await this.#sendForReply<DeleteKeyValueResponse>(context, payload);
return result.deleted;
},
},

View File

@@ -0,0 +1,12 @@
{
"name": "@yaak/template-function-ctx",
"displayName": "Window Template Functions",
"description": "Template functions for accessing attributes of the current window",
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
}
}

View File

@@ -0,0 +1,30 @@
import type { PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'ctx.request',
description: 'Get the ID of the currently active request',
args: [],
async onRender(ctx) {
return ctx.window.requestId();
},
},
{
name: 'ctx.environment',
description: 'Get the ID of the currently active environment',
args: [],
async onRender(ctx) {
return ctx.window.environmentId();
},
},
{
name: 'ctx.workspace',
description: 'Get the ID of the currently active workspace',
args: [],
async onRender(ctx) {
return ctx.window.workspaceId();
},
},
],
};

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -58,7 +58,6 @@ export const plugin: PluginDefinition = {
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
try {
console.log('formatted', args.values.formatted);
return filterJSONPath(
String(args.values.input),
String(args.values.query),

View File

@@ -7,6 +7,9 @@
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
},
"dependencies": {
"slugify": "^1.6.6"
}
}

View File

@@ -1,25 +1,174 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import slugify from 'slugify';
const STORE_NONE = 'none';
const STORE_FOREVER = 'forever';
const STORE_EXPIRE = 'expire';
interface Saved {
value: string;
createdAt: number;
}
export const plugin: PluginDefinition = {
templateFunctions: [{
name: 'prompt.text',
description: 'Prompt the user for input when sending a request',
args: [
{ type: 'text', name: 'title', label: 'Title' },
{ type: 'text', name: 'label', label: 'Label', optional: true },
{ type: 'text', name: 'defaultValue', label: 'Default Value', optional: true },
{ type: 'text', name: 'placeholder', label: 'Placeholder', optional: true },
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (args.purpose !== 'send') return null;
templateFunctions: [
{
name: 'prompt.text',
description: 'Prompt the user for input when sending a request',
previewType: 'click',
args: [
{ type: 'text', name: 'label', label: 'Label' },
{
type: 'select',
name: 'store',
label: 'Store Input',
defaultValue: STORE_NONE,
options: [
{ label: 'Never', value: STORE_NONE },
{ label: 'Expire', value: STORE_EXPIRE },
{ label: 'Forever', value: STORE_FOREVER },
],
},
{
type: 'h_stack',
dynamic(_ctx, args) {
return { hidden: args.values.store === STORE_NONE };
},
inputs: [
{
type: 'text',
name: 'namespace',
label: 'Namespace',
defaultValue: '${[ctx.workspace()]}',
optional: true,
},
{
type: 'text',
name: 'key',
label: 'Key (defaults to Label)',
optional: true,
dynamic(_ctx, args) {
return { placeholder: String(args.values.label || '') };
},
},
{
type: 'text',
name: 'ttl',
label: 'TTL (seconds)',
placeholder: '0',
defaultValue: '0',
optional: true,
dynamic(_ctx, args) {
return { hidden: args.values.store !== STORE_EXPIRE };
},
},
],
},
{
type: 'banner',
color: 'info',
dynamic(_ctx, args) {
return { hidden: args.values.store === STORE_NONE };
},
inputs: [
{
type: 'markdown',
content: '',
async dynamic(_ctx, args) {
const key = buildKey(args);
return {
content: ['Value will be saved under: `' + key + '`'].join('\n\n'),
};
},
},
],
},
{
type: 'accordion',
label: 'Advanced',
inputs: [
{
type: 'text',
name: 'title',
label: 'Prompt Title',
optional: true,
placeholder: 'Enter Value',
},
{ type: 'text', name: 'defaultValue', label: 'Default Value', optional: true },
{ type: 'text', name: 'placeholder', label: 'Input Placeholder', optional: true },
{ type: 'checkbox', name: 'password', label: 'Mask Value' },
],
},
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (args.purpose !== 'send') return null;
return await ctx.prompt.text({
id: `prompt-${args.values.label}`,
label: String(args.values.title ?? ''),
title: String(args.values.title ?? ''),
defaultValue: String(args.values.defaultValue),
placeholder: String(args.values.placeholder),
});
if (args.values.store !== STORE_NONE && !args.values.namespace) {
throw new Error('Namespace is required when storing values')
}
const existing = await maybeGetValue(ctx, args);
if (existing != null) {
return existing;
}
const value = await ctx.prompt.text({
id: `prompt-${args.values.label ?? 'none'}`,
label: String(args.values.label || 'Value'),
title: String(args.values.title ?? 'Enter Value'),
defaultValue: String(args.values.defaultValue ?? ''),
placeholder: String(args.values.placeholder ?? ''),
password: Boolean(args.values.password),
required: false,
});
if (value == null) {
throw new Error('Prompt cancelled');
}
if (args.values.store !== STORE_NONE) {
await maybeSetValue(ctx, args, value);
}
return value;
},
},
}],
],
};
function buildKey(args: CallTemplateFunctionArgs) {
return [args.values.namespace, args.values.key || args.values.label]
.filter((v) => !!v)
.map((v) => slugify(String(v), { lower: true, trim: true }))
.join('.');
}
async function maybeGetValue(ctx: Context, args: CallTemplateFunctionArgs) {
if (args.values.store === STORE_NONE) return null;
const existing = await ctx.store.get<Saved>(buildKey(args));
if (existing == null) {
return null;
}
if (args.values.store === STORE_FOREVER) {
return existing.value;
}
const ttlSeconds = parseInt(String(args.values.ttl)) || 0;
const ageSeconds = (Date.now() - existing.createdAt) / 1000;
if (ageSeconds > ttlSeconds) {
ctx.store.delete(buildKey(args)).catch(console.error);
return null;
}
return existing.value;
}
async function maybeSetValue(ctx: Context, args: CallTemplateFunctionArgs, value: string) {
if (args.values.store === STORE_NONE) {
return;
}
await ctx.store.set<Saved>(buildKey(args), { value, createdAt: Date.now() });
}

View File

@@ -117,6 +117,7 @@ async fn cmd_render_template<R: Runtime>(
template: &str,
workspace_id: &str,
environment_id: Option<&str>,
purpose: Option<RenderPurpose>,
) -> YaakResult<String> {
let environment_chain =
app_handle.db().resolve_environments(workspace_id, None, environment_id)?;
@@ -126,7 +127,7 @@ async fn cmd_render_template<R: Runtime>(
&PluginTemplateCallback::new(
&app_handle,
&PluginContext::new(&window),
RenderPurpose::Preview,
purpose.unwrap_or(RenderPurpose::Preview),
),
&RenderOptions {
error_behavior: RenderErrorBehavior::Throw,

View File

@@ -16,12 +16,13 @@ use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::queries::any_request::AnyRequest;
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
use yaak_plugins::error::Error::PluginErr;
use yaak_plugins::events::{
Color, DeleteKeyValueResponse, EmptyPayload, ErrorResponse, FindHttpResponsesResponse,
GetCookieValueResponse, GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent,
InternalEventPayload, ListCookieNamesResponse, RenderGrpcRequestResponse,
RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest,
TemplateRenderResponse, WindowNavigateEvent,
TemplateRenderResponse, WindowInfoResponse, WindowNavigateEvent,
};
use yaak_plugins::plugin_handle::PluginHandle;
use yaak_plugins::template_callback::PluginTemplateCallback;
@@ -334,6 +335,29 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
};
Ok(Some(InternalEventPayload::GetCookieValueResponse(GetCookieValueResponse { value })))
}
InternalEventPayload::WindowInfoRequest(req) => {
let w = app_handle
.get_webview_window(&req.label)
.ok_or(PluginErr(format!("Failed to find window for {}", req.label)))?;
// Actually look up the data so we never return an invalid ID
let environment_id = environment_from_window(&w).map(|m| m.id);
let workspace_id = workspace_from_window(&w).map(|m| m.id);
let request_id =
match app_handle.db().get_any_request(&w.request_id().unwrap_or_default()) {
Ok(AnyRequest::HttpRequest(r)) => Some(r.id),
Ok(AnyRequest::WebsocketRequest(r)) => Some(r.id),
Ok(AnyRequest::GrpcRequest(r)) => Some(r.id),
Err(_) => None,
};
Ok(Some(InternalEventPayload::WindowInfoResponse(WindowInfoResponse {
label: w.label().to_string(),
request_id,
workspace_id,
environment_id,
})))
}
_ => Ok(None),
}
}

View File

@@ -224,7 +224,7 @@ defaultValue?: string, disabled?: boolean,
*/
description?: string, };
export type FormInputHStack = { inputs?: Array<FormInput>, };
export type FormInputHStack = { inputs?: Array<FormInput>, hidden?: boolean, };
export type FormInputHttpRequest = {
/**
@@ -391,7 +391,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, context: PluginContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "window_info_request" } & WindowInfoRequest | { "type": "window_info_response" } & WindowInfoResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;
@@ -411,7 +411,7 @@ export type PromptTextRequest = { id: string, title: string, label: string, desc
/**
* Text to add to the confirmation button
*/
confirmText?: string,
confirmText?: string, password?: boolean,
/**
* Text to add to the cancel button
*/
@@ -445,7 +445,7 @@ export type SetKeyValueResponse = {};
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, timeout?: number, };
export type TemplateFunction = { name: string, description?: string,
export type TemplateFunction = { name: string, previewType?: TemplateFunctionPreviewType, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
@@ -457,6 +457,8 @@ aliases?: Array<string>, args: Array<TemplateFunctionArg>, };
*/
export type TemplateFunctionArg = FormInput;
export type TemplateFunctionPreviewType = "live" | "click" | "none";
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
@@ -487,6 +489,10 @@ export type ThemeComponentColors = { surface?: string, surfaceHighlight?: string
export type ThemeComponents = { dialog?: ThemeComponentColors, menu?: ThemeComponentColors, toast?: ThemeComponentColors, sidebar?: ThemeComponentColors, responsePane?: ThemeComponentColors, appHeader?: ThemeComponentColors, button?: ThemeComponentColors, banner?: ThemeComponentColors, templateTag?: ThemeComponentColors, urlBar?: ThemeComponentColors, editor?: ThemeComponentColors, input?: ThemeComponentColors, };
export type WindowInfoRequest = { label: string, };
export type WindowInfoResponse = { requestId: string | null, environmentId: string | null, workspaceId: string | null, label: string, };
export type WindowNavigateEvent = { url: string, };
export type WindowSize = { width: number, height: number, };

View File

@@ -147,6 +147,9 @@ pub enum InternalEventPayload {
PromptTextRequest(PromptTextRequest),
PromptTextResponse(PromptTextResponse),
WindowInfoRequest(WindowInfoRequest),
WindowInfoResponse(WindowInfoResponse),
GetHttpRequestByIdRequest(GetHttpRequestByIdRequest),
GetHttpRequestByIdResponse(GetHttpRequestByIdResponse),
@@ -521,6 +524,8 @@ pub struct PromptTextRequest {
/// Text to add to the confirmation button
#[ts(optional)]
pub confirm_text: Option<String>,
#[ts(optional)]
pub password: Option<bool>,
/// Text to add to the cancel button
#[ts(optional)]
pub cancel_text: Option<String>,
@@ -536,6 +541,23 @@ pub struct PromptTextResponse {
pub value: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct WindowInfoRequest {
pub label: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct WindowInfoResponse {
pub request_id: Option<String>,
pub environment_id: Option<String>,
pub workspace_id: Option<String>,
pub label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "gen_events.ts")]
@@ -710,12 +732,24 @@ pub struct GetTemplateFunctionConfigResponse {
pub plugin_ref_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "gen_events.ts")]
pub enum TemplateFunctionPreviewType {
Live,
Click,
None,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct TemplateFunction {
pub name: String,
#[ts(optional)]
pub preview_type: Option<TemplateFunctionPreviewType>,
#[ts(optional)]
pub description: Option<String>,
@@ -972,6 +1006,9 @@ pub struct FormInputAccordion {
pub struct FormInputHStack {
#[ts(optional)]
pub inputs: Option<Vec<FormInput>>,
#[ts(optional)]
pub hidden: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]

View File

@@ -1,6 +1,6 @@
use crate::events::{
FormInput, FormInputBase, FormInputText, PluginContext, RenderPurpose, TemplateFunction,
TemplateFunctionArg,
TemplateFunctionArg, TemplateFunctionPreviewType,
};
use crate::template_callback::PluginTemplateCallback;
use base64::Engine;
@@ -17,6 +17,7 @@ use yaak_templates::{FnArg, Parser, Token, Tokens, Val, transform_args};
pub(crate) fn template_function_secure() -> TemplateFunction {
TemplateFunction {
name: "secure".to_string(),
preview_type: Some(TemplateFunctionPreviewType::None),
description: Some("Securely store encrypted text".to_string()),
aliases: None,
args: vec![TemplateFunctionArg::FormInput(FormInput::Text(
@@ -37,6 +38,7 @@ pub(crate) fn template_function_secure() -> TemplateFunction {
pub(crate) fn template_function_keyring() -> TemplateFunction {
TemplateFunction {
name: "keychain".to_string(),
preview_type: Some(TemplateFunctionPreviewType::Live),
description: Some("Get a password from the OS keychain or keyring".to_string()),
aliases: Some(vec!["keyring".to_string()]),
args: vec![

View File

@@ -30,7 +30,6 @@ export const createFolder = createFastMutation<
label: 'Name',
defaultValue: 'Folder',
title: 'New Folder',
required: true,
confirmText: 'Create',
placeholder: 'Name',
});

View File

@@ -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 (

View File

@@ -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',

View File

@@ -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 || <>&nbsp;</>
)}
<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);

View File

@@ -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>
);
}

View File

@@ -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}

View File

@@ -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',

View File

@@ -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}

View File

@@ -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({

View File

@@ -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);

View File

@@ -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'

View File

@@ -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;
},
});
}