Add previewArgs support for template functions and enhance validation logic for form inputs

This commit is contained in:
Gregory Schier
2025-11-27 12:55:39 -08:00
parent 0c7034eefc
commit 8d1b17cac1
24 changed files with 340 additions and 92 deletions

View File

@@ -28,6 +28,7 @@ export const plugin: PluginDefinition = {
{
name: '1password.item',
description: 'Get a secret',
previewArgs: ['field'],
args: [
{
name: 'token',

View File

@@ -5,15 +5,18 @@ export const plugin: PluginDefinition = {
{
name: 'cookie.value',
description: 'Read the value of a cookie in the jar, by name',
previewArgs: ['name'],
args: [
{
type: 'text',
name: 'cookie_name',
name: 'name',
label: 'Cookie Name',
},
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return ctx.cookies.getValue({ name: String(args.values.cookie_name) });
// The legacy name was cookie_name, but we changed it
const name = args.values.cookie_name ?? args.values.name;
return ctx.cookies.getValue({ name: String(name) });
},
},
],

View File

@@ -16,6 +16,7 @@ export const plugin: PluginDefinition = {
{
name: 'fs.readFile',
description: 'Read the contents of a file as utf-8',
previewArgs: ['encoding'],
args: [
{ title: 'Select File', type: 'file', name: 'path', label: 'File' },
{
@@ -26,14 +27,21 @@ export const plugin: PluginDefinition = {
description: "Specifies how the file's bytes are decoded into text when read",
options,
},
{
type: 'checkbox',
name: 'trim',
label: 'Trim Whitespace',
description: 'Remove leading and trailing whitespace from the file contents',
},
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.path || !args.values.encoding) return null;
try {
return fs.promises.readFile(String(args.values.path ?? ''), {
const v = await fs.promises.readFile(String(args.values.path ?? ''), {
encoding: String(args.values.encoding ?? 'utf-8') as BufferEncoding,
});
return args.values.trim ? v.trim() : v;
} catch {
return null;
}

View File

@@ -11,6 +11,7 @@ export const plugin: PluginDefinition = {
{
name: 'json.jsonpath',
description: 'Filter JSON-formatted text using JSONPath syntax',
previewArgs: ['query'],
args: [
{
type: 'editor',

View File

@@ -16,8 +16,22 @@ export const plugin: PluginDefinition = {
name: 'prompt.text',
description: 'Prompt the user for input when sending a request',
previewType: 'click',
previewArgs: ['label'],
args: [
{ type: 'text', name: 'label', label: 'Label', optional: true },
{
type: 'text',
name: 'label',
label: 'Label',
optional: true,
dynamic(_ctx, args) {
if (
args.values.store === STORE_EXPIRE ||
(args.values.store === STORE_FOREVER && !args.values.key)
) {
return { optional: false };
}
},
},
{
type: 'select',
name: 'store',
@@ -68,21 +82,24 @@ export const plugin: PluginDefinition = {
{
type: 'banner',
color: 'info',
inputs: [],
dynamic(_ctx, args) {
return { hidden: args.values.store === STORE_NONE };
},
inputs: [
{
type: 'markdown',
content: '',
async dynamic(_ctx, args) {
const key = buildKey(args);
return {
let key: string;
try {
key = buildKey(args);
} catch (err) {
return { color: 'danger', inputs: [{ type: 'markdown', content: String(err) }] };
}
return {
hidden: args.values.store === STORE_NONE,
inputs: [
{
type: 'markdown',
content: [`Value will be saved under: \`${key}\``].join('\n\n'),
};
},
},
],
},
],
};
},
},
{
type: 'accordion',
@@ -139,7 +156,7 @@ export const plugin: PluginDefinition = {
function buildKey(args: CallTemplateFunctionArgs) {
if (!args.values.key && !args.values.label) {
throw new Error('Key or Label is required when storing values');
throw new Error('A label or key is required when storing values');
}
return [args.values.namespace, args.values.key || args.values.label]
.filter((v) => !!v)

View File

@@ -5,6 +5,7 @@ export const plugin: PluginDefinition = {
{
name: 'random.range',
description: 'Generate a random number between two values',
previewArgs: ['min', 'max'],
args: [
{
type: 'text',

View File

@@ -24,6 +24,7 @@ export const plugin: PluginDefinition = {
name: 'regex.match',
description: 'Extract text using a regular expression',
args: [inputArg, regexArg],
previewArgs: [regexArg.name],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
const input = String(args.values.input ?? '');
const regex = new RegExp(String(args.values.regex ?? ''));
@@ -37,6 +38,7 @@ export const plugin: PluginDefinition = {
{
name: 'regex.replace',
description: 'Replace text using a regular expression',
previewArgs: [regexArg.name],
args: [
inputArg,
regexArg,

View File

@@ -1,11 +1,20 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import type { AnyModel, HttpUrlParameter } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import type { JSONPathResult } from '../../template-function-json';
import { filterJSONPath } from '../../template-function-json';
import type { XPathResult } from '../../template-function-xml';
import { filterXPath } from '../../template-function-xml';
const RETURN_FIRST = 'first';
const RETURN_ALL = 'all';
const RETURN_JOIN = 'join';
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'request.body',
name: 'request.body.raw',
aliases: ['request.body'],
args: [
{
name: 'requestId',
@@ -25,8 +34,115 @@ export const plugin: PluginDefinition = {
);
},
},
{
name: 'request.body.path',
previewArgs: ['path'],
args: [
{ name: 'requestId', label: 'Http Request', type: 'http_request' },
{
type: 'h_stack',
inputs: [
{
type: 'select',
name: 'result',
label: 'Return Format',
defaultValue: RETURN_FIRST,
options: [
{ label: 'First result', value: RETURN_FIRST },
{ label: 'All results', value: RETURN_ALL },
{ label: 'Join with separator', value: RETURN_JOIN },
],
},
{
name: 'join',
type: 'text',
label: 'Separator',
optional: true,
defaultValue: ', ',
dynamic(_ctx, args) {
return { hidden: args.values.result !== RETURN_JOIN };
},
},
],
},
{
type: 'text',
name: 'path',
label: 'JSONPath or XPath',
placeholder: '$.books[0].id or /books[0]/id',
dynamic: async (ctx, args) => {
const requestId = String(args.values.requestId ?? 'n/a');
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
if (httpRequest == null) return null;
const contentType =
httpRequest.headers
.find((h) => h.name.toLowerCase() === 'content-type')
?.value.toLowerCase() ?? '';
if (contentType.includes('xml') || contentType?.includes('html')) {
return {
label: 'XPath',
placeholder: '/books[0]/id',
description: 'Enter an XPath expression used to filter the results',
};
}
return {
label: 'JSONPath',
placeholder: '$.books[0].id',
description: 'Enter a JSONPath expression used to filter the results',
};
},
},
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
const requestId = String(args.values.requestId ?? 'n/a');
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
if (httpRequest == null) return null;
const body = httpRequest.body?.text ?? '';
try {
const result: JSONPathResult =
args.values.result === RETURN_ALL
? 'all'
: args.values.result === RETURN_JOIN
? 'join'
: 'first';
return filterJSONPath(
body,
String(args.values.path || ''),
result,
args.values.join == null ? null : String(args.values.join),
);
} catch {
// Probably not JSON, try XPath
}
try {
const result: XPathResult =
args.values.result === RETURN_ALL
? 'all'
: args.values.result === RETURN_JOIN
? 'join'
: 'first';
return filterXPath(
body,
String(args.values.path || ''),
result,
args.values.join == null ? null : String(args.values.join),
);
} catch {
// Probably not XML
}
return null; // Bail out
},
},
{
name: 'request.header',
description: 'Read the value of a request header, by name',
previewArgs: ['header'],
args: [
{
name: 'requestId',

View File

@@ -62,6 +62,7 @@ export const plugin: PluginDefinition = {
{
name: 'response.header',
description: 'Read the value of a response header, by name',
previewArgs: ['header'],
args: [
requestArg,
behaviorArgs,
@@ -108,6 +109,7 @@ export const plugin: PluginDefinition = {
name: 'response.body.path',
description: 'Access a field of the response body using JsonPath or XPath',
aliases: ['response'],
previewArgs: ['path'],
args: [
requestArg,
behaviorArgs,
@@ -155,7 +157,9 @@ export const plugin: PluginDefinition = {
}
const contentType =
resp?.headers.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? '';
resp?.headers
.find((h) => h.name.toLowerCase() === 'content-type')
?.value.toLowerCase() ?? '';
if (contentType.includes('xml') || contentType?.includes('html')) {
return {
label: 'XPath',
@@ -187,9 +191,10 @@ export const plugin: PluginDefinition = {
return null;
}
const BOM = '\ufeff';
let body: string;
try {
body = readFileSync(response.bodyPath, 'utf-8');
body = readFileSync(response.bodyPath, 'utf-8').replace(BOM, '');
} catch {
return null;
}

View File

@@ -81,12 +81,14 @@ export const plugin: PluginDefinition = {
name: 'timestamp.format',
description: 'Format a date using a dayjs-compatible format string',
args: [dateArg, formatArg],
previewArgs: [formatArg.name],
onRender: async (_ctx, args) => formatDatetime(args.values),
},
{
name: 'timestamp.offset',
description: 'Get the offset of a date based on an expression',
args: [dateArg, expressionArg],
previewArgs: [expressionArg.name],
onRender: async (_ctx, args) => calculateDatetime(args.values),
},
],

View File

@@ -11,6 +11,7 @@ export const plugin: PluginDefinition = {
{
name: 'xml.xpath',
description: 'Filter XML-formatted text using XPath syntax',
previewArgs: ['query'],
args: [
{
type: 'text',