mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 09:48:28 +02:00
Add an option to allow jsonpath/xpath to return as array (#297)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@@ -19146,12 +19146,7 @@
|
|||||||
"name": "@yaak/template-function-response",
|
"name": "@yaak/template-function-response",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xmldom/xmldom": "^0.9.8",
|
"@yaak/template-function-xml": "*"
|
||||||
"jsonpath-plus": "^10.3.0",
|
|
||||||
"xpath": "^0.0.34"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/jsonpath": "^0.2.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins/template-function-timestamp": {
|
"plugins/template-function-timestamp": {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
|
|||||||
|
|
||||||
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
|
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
|
||||||
|
|
||||||
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: JsonValue }, };
|
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: JsonPrimitive }, };
|
||||||
|
|
||||||
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
|
|||||||
|
|
||||||
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
|
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
|
||||||
|
|
||||||
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
|
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
|
||||||
|
|
||||||
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
|
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
|
||||||
|
|
||||||
@@ -224,6 +224,8 @@ defaultValue?: string, disabled?: boolean,
|
|||||||
*/
|
*/
|
||||||
description?: string, };
|
description?: string, };
|
||||||
|
|
||||||
|
export type FormInputHStack = { inputs?: Array<FormInput>, };
|
||||||
|
|
||||||
export type FormInputHttpRequest = {
|
export type FormInputHttpRequest = {
|
||||||
/**
|
/**
|
||||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
|
|||||||
@@ -3,22 +3,37 @@ import {
|
|||||||
CallHttpAuthenticationRequest,
|
CallHttpAuthenticationRequest,
|
||||||
CallHttpAuthenticationResponse,
|
CallHttpAuthenticationResponse,
|
||||||
FormInput,
|
FormInput,
|
||||||
GetHttpAuthenticationConfigRequest,
|
|
||||||
GetHttpAuthenticationSummaryResponse,
|
GetHttpAuthenticationSummaryResponse,
|
||||||
HttpAuthenticationAction,
|
HttpAuthenticationAction,
|
||||||
} from '../bindings/gen_events';
|
} from '../bindings/gen_events';
|
||||||
import { MaybePromise } from '../helpers';
|
import { MaybePromise } from '../helpers';
|
||||||
import { Context } from './Context';
|
import { Context } from './Context';
|
||||||
|
|
||||||
type DynamicFormInput = FormInput & {
|
type AddDynamicMethod<T> = {
|
||||||
dynamic(
|
dynamic?: (
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
args: GetHttpAuthenticationConfigRequest,
|
args: CallHttpAuthenticationActionArgs,
|
||||||
): MaybePromise<Partial<FormInput> | undefined | null>;
|
) => MaybePromise<Partial<T> | null | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AddDynamic<T> = T extends any
|
||||||
|
? T extends { inputs?: FormInput[] }
|
||||||
|
? Omit<T, 'inputs'> & {
|
||||||
|
inputs: Array<AddDynamic<FormInput>>;
|
||||||
|
dynamic?: (
|
||||||
|
ctx: Context,
|
||||||
|
args: CallHttpAuthenticationActionArgs,
|
||||||
|
) => MaybePromise<
|
||||||
|
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
: T & AddDynamicMethod<T>
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type DynamicAuthenticationArg = AddDynamic<FormInput>;
|
||||||
|
|
||||||
export type AuthenticationPlugin = GetHttpAuthenticationSummaryResponse & {
|
export type AuthenticationPlugin = GetHttpAuthenticationSummaryResponse & {
|
||||||
args: (FormInput | DynamicFormInput)[];
|
args: DynamicAuthenticationArg[];
|
||||||
onApply(
|
onApply(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
args: CallHttpAuthenticationRequest,
|
args: CallHttpAuthenticationRequest,
|
||||||
|
|||||||
@@ -1,21 +1,31 @@
|
|||||||
import {
|
import { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events';
|
||||||
CallTemplateFunctionArgs,
|
|
||||||
FormInput,
|
|
||||||
GetHttpAuthenticationConfigRequest,
|
|
||||||
TemplateFunction,
|
|
||||||
TemplateFunctionArg,
|
|
||||||
} from '../bindings/gen_events';
|
|
||||||
import { MaybePromise } from '../helpers';
|
import { MaybePromise } from '../helpers';
|
||||||
import { Context } from './Context';
|
import { Context } from './Context';
|
||||||
|
|
||||||
export type DynamicTemplateFunctionArg = FormInput & {
|
type AddDynamicMethod<T> = {
|
||||||
dynamic(
|
dynamic?: (
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
args: GetHttpAuthenticationConfigRequest,
|
args: CallTemplateFunctionArgs,
|
||||||
): MaybePromise<Partial<FormInput> | undefined | null>;
|
) => MaybePromise<Partial<T> | null | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TemplateFunctionPlugin = TemplateFunction & {
|
type AddDynamic<T> = T extends any
|
||||||
args: (TemplateFunctionArg | DynamicTemplateFunctionArg)[];
|
? T extends { inputs?: FormInput[] }
|
||||||
|
? Omit<T, 'inputs'> & {
|
||||||
|
inputs: Array<AddDynamic<FormInput>>;
|
||||||
|
dynamic?: (
|
||||||
|
ctx: Context,
|
||||||
|
args: CallTemplateFunctionArgs,
|
||||||
|
) => MaybePromise<
|
||||||
|
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
: T & AddDynamicMethod<T>
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type DynamicTemplateFunctionArg = AddDynamic<FormInput>;
|
||||||
|
|
||||||
|
export type TemplateFunctionPlugin = Omit<TemplateFunction, 'args'> & {
|
||||||
|
args: DynamicTemplateFunctionArg[];
|
||||||
onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>;
|
onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { AuthenticationPlugin } from './AuthenticationPlugin';
|
import { AuthenticationPlugin } from './AuthenticationPlugin';
|
||||||
|
|
||||||
|
import type { Context } from './Context';
|
||||||
import type { FilterPlugin } from './FilterPlugin';
|
import type { FilterPlugin } from './FilterPlugin';
|
||||||
import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
|
import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
|
||||||
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
|
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
|
||||||
@@ -6,9 +8,10 @@ import type { ImporterPlugin } from './ImporterPlugin';
|
|||||||
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
|
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
|
||||||
import type { ThemePlugin } from './ThemePlugin';
|
import type { ThemePlugin } from './ThemePlugin';
|
||||||
|
|
||||||
import type { Context } from './Context';
|
|
||||||
|
|
||||||
export type { Context };
|
export type { Context };
|
||||||
|
export type { DynamicTemplateFunctionArg } from './TemplateFunctionPlugin';
|
||||||
|
export type { DynamicAuthenticationArg } from './AuthenticationPlugin';
|
||||||
|
export type { TemplateFunctionPlugin };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The global structure of a Yaak plugin
|
* The global structure of a Yaak plugin
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
BootRequest,
|
BootRequest,
|
||||||
DeleteKeyValueResponse,
|
DeleteKeyValueResponse,
|
||||||
FindHttpResponsesResponse,
|
FindHttpResponsesResponse,
|
||||||
FormInput,
|
|
||||||
GetCookieValueRequest,
|
GetCookieValueRequest,
|
||||||
GetCookieValueResponse,
|
GetCookieValueResponse,
|
||||||
GetHttpRequestByIdResponse,
|
GetHttpRequestByIdResponse,
|
||||||
@@ -19,14 +18,13 @@ import {
|
|||||||
RenderHttpRequestResponse,
|
RenderHttpRequestResponse,
|
||||||
SendHttpRequestResponse,
|
SendHttpRequestResponse,
|
||||||
TemplateFunction,
|
TemplateFunction,
|
||||||
TemplateFunctionArg,
|
|
||||||
TemplateRenderResponse,
|
TemplateRenderResponse,
|
||||||
} from '@yaakapp-internal/plugins';
|
} from '@yaakapp-internal/plugins';
|
||||||
import { Context, PluginDefinition } from '@yaakapp/api';
|
import { Context, PluginDefinition } from '@yaakapp/api';
|
||||||
import { JsonValue } from '@yaakapp/api/lib/bindings/serde_json/JsonValue';
|
|
||||||
import console from 'node:console';
|
import console from 'node:console';
|
||||||
import { type Stats, statSync, watch } from 'node:fs';
|
import { type Stats, statSync, watch } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { applyDynamicFormInput, applyFormInputDefaults } from './common';
|
||||||
import { EventChannel } from './EventChannel';
|
import { EventChannel } from './EventChannel';
|
||||||
import { migrateTemplateFunctionSelectOptions } from './migrations';
|
import { migrateTemplateFunctionSelectOptions } from './migrations';
|
||||||
|
|
||||||
@@ -213,24 +211,19 @@ export class PluginInstance {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
templateFunction = migrateTemplateFunctionSelectOptions(templateFunction);
|
const fn = {
|
||||||
// @ts-ignore
|
...migrateTemplateFunctionSelectOptions(templateFunction),
|
||||||
delete templateFunction.onRender;
|
onRender: undefined,
|
||||||
const resolvedArgs: TemplateFunctionArg[] = [];
|
};
|
||||||
for (const arg of templateFunction.args) {
|
|
||||||
if (arg && 'dynamic' in arg) {
|
payload.values = applyFormInputDefaults(fn.args, payload.values);
|
||||||
const dynamicAttrs = await arg.dynamic(ctx, payload);
|
const p = { ...payload, purpose: 'preview' } as const;
|
||||||
const { dynamic, ...other } = arg;
|
const resolvedArgs = await applyDynamicFormInput(ctx, fn.args, p);
|
||||||
resolvedArgs.push({ ...other, ...dynamicAttrs } as TemplateFunctionArg);
|
|
||||||
} else if (arg) {
|
|
||||||
resolvedArgs.push(arg);
|
|
||||||
}
|
|
||||||
templateFunction.args = resolvedArgs;
|
|
||||||
}
|
|
||||||
const replyPayload: InternalEventPayload = {
|
const replyPayload: InternalEventPayload = {
|
||||||
type: 'get_template_function_config_response',
|
type: 'get_template_function_config_response',
|
||||||
pluginRefId: this.#workerData.pluginRefId,
|
pluginRefId: this.#workerData.pluginRefId,
|
||||||
function: templateFunction,
|
function: { ...fn, args: resolvedArgs },
|
||||||
};
|
};
|
||||||
this.#sendPayload(context, replyPayload, replyId);
|
this.#sendPayload(context, replyPayload, replyId);
|
||||||
return;
|
return;
|
||||||
@@ -248,16 +241,8 @@ export class PluginInstance {
|
|||||||
|
|
||||||
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
|
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
|
||||||
const { args, actions } = this.#mod.authentication;
|
const { args, actions } = this.#mod.authentication;
|
||||||
const resolvedArgs: FormInput[] = [];
|
payload.values = applyFormInputDefaults(args, payload.values);
|
||||||
for (const v of args) {
|
const resolvedArgs = await applyDynamicFormInput(ctx, args, payload);
|
||||||
if (v && 'dynamic' in v) {
|
|
||||||
const dynamicAttrs = await v.dynamic(ctx, payload);
|
|
||||||
const { dynamic, ...other } = v;
|
|
||||||
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
|
|
||||||
} else if (v) {
|
|
||||||
resolvedArgs.push(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const resolvedActions: HttpAuthenticationAction[] = [];
|
const resolvedActions: HttpAuthenticationAction[] = [];
|
||||||
for (const { onSelect, ...action } of actions ?? []) {
|
for (const { onSelect, ...action } of actions ?? []) {
|
||||||
resolvedActions.push(action);
|
resolvedActions.push(action);
|
||||||
@@ -277,7 +262,8 @@ export class PluginInstance {
|
|||||||
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) {
|
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) {
|
||||||
const auth = this.#mod.authentication;
|
const auth = this.#mod.authentication;
|
||||||
if (typeof auth?.onApply === 'function') {
|
if (typeof auth?.onApply === 'function') {
|
||||||
applyFormInputDefaults(auth.args, payload.values);
|
auth.args = await applyDynamicFormInput(ctx, auth.args, payload);
|
||||||
|
payload.values = applyFormInputDefaults(auth.args, payload.values);
|
||||||
this.#sendPayload(
|
this.#sendPayload(
|
||||||
context,
|
context,
|
||||||
{
|
{
|
||||||
@@ -332,7 +318,8 @@ export class PluginInstance {
|
|||||||
) {
|
) {
|
||||||
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
|
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
|
||||||
if (typeof fn?.onRender === 'function') {
|
if (typeof fn?.onRender === 'function') {
|
||||||
applyFormInputDefaults(fn.args, payload.args.values);
|
const resolvedArgs = await applyDynamicFormInput(ctx, fn.args, payload.args);
|
||||||
|
payload.args.values = applyFormInputDefaults(resolvedArgs, payload.args.values);
|
||||||
try {
|
try {
|
||||||
const result = await fn.onRender(ctx, payload.args);
|
const result = await fn.onRender(ctx, payload.args);
|
||||||
this.#sendPayload(
|
this.#sendPayload(
|
||||||
@@ -652,20 +639,6 @@ function genId(len = 5): string {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Recursively apply form input defaults to a set of values */
|
|
||||||
function applyFormInputDefaults(
|
|
||||||
inputs: TemplateFunctionArg[],
|
|
||||||
values: { [p: string]: JsonValue | undefined },
|
|
||||||
) {
|
|
||||||
for (const input of inputs) {
|
|
||||||
if ('inputs' in input) {
|
|
||||||
applyFormInputDefaults(input.inputs ?? [], values);
|
|
||||||
} else if ('defaultValue' in input && values[input.name] === undefined) {
|
|
||||||
values[input.name] = input.defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const watchedFiles: Record<string, Stats | null> = {};
|
const watchedFiles: Record<string, Stats | null> = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
56
packages/plugin-runtime/src/common.ts
Normal file
56
packages/plugin-runtime/src/common.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
CallHttpAuthenticationActionArgs,
|
||||||
|
CallTemplateFunctionArgs,
|
||||||
|
JsonPrimitive,
|
||||||
|
TemplateFunctionArg,
|
||||||
|
} from '@yaakapp-internal/plugins';
|
||||||
|
import { Context, DynamicAuthenticationArg, DynamicTemplateFunctionArg } from '@yaakapp/api';
|
||||||
|
|
||||||
|
/** Recursively apply form input defaults to a set of values */
|
||||||
|
export function applyFormInputDefaults(
|
||||||
|
inputs: TemplateFunctionArg[],
|
||||||
|
values: { [p: string]: JsonPrimitive | undefined },
|
||||||
|
) {
|
||||||
|
let newValues: { [p: string]: JsonPrimitive | undefined } = { ...values };
|
||||||
|
for (const input of inputs) {
|
||||||
|
if ('defaultValue' in input && values[input.name] === undefined) {
|
||||||
|
newValues[input.name] = input.defaultValue;
|
||||||
|
}
|
||||||
|
// Recurse down to all child inputs
|
||||||
|
if ('inputs' in input) {
|
||||||
|
newValues = applyFormInputDefaults(input.inputs ?? [], newValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyDynamicFormInput(
|
||||||
|
ctx: Context,
|
||||||
|
args: DynamicTemplateFunctionArg[],
|
||||||
|
callArgs: CallTemplateFunctionArgs,
|
||||||
|
): Promise<DynamicTemplateFunctionArg[]>;
|
||||||
|
|
||||||
|
export async function applyDynamicFormInput(
|
||||||
|
ctx: Context,
|
||||||
|
args: DynamicAuthenticationArg[],
|
||||||
|
callArgs: CallHttpAuthenticationActionArgs,
|
||||||
|
): Promise<DynamicAuthenticationArg[]>;
|
||||||
|
|
||||||
|
export async function applyDynamicFormInput(
|
||||||
|
ctx: Context,
|
||||||
|
args: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[],
|
||||||
|
callArgs: CallTemplateFunctionArgs | CallHttpAuthenticationActionArgs,
|
||||||
|
): Promise<(DynamicTemplateFunctionArg | DynamicAuthenticationArg)[]> {
|
||||||
|
const resolvedArgs: any[] = [];
|
||||||
|
for (const { dynamic, ...arg } of args) {
|
||||||
|
const newArg: any = {
|
||||||
|
...arg,
|
||||||
|
...(typeof dynamic === 'function' ? await dynamic(ctx, callArgs as any) : undefined),
|
||||||
|
};
|
||||||
|
if ('inputs' in newArg && Array.isArray(newArg.inputs)) {
|
||||||
|
newArg.inputs = await applyDynamicFormInput(ctx, newArg.inputs, callArgs as any);
|
||||||
|
}
|
||||||
|
resolvedArgs.push(newArg);
|
||||||
|
}
|
||||||
|
return resolvedArgs;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
|
import type { TemplateFunctionPlugin } from '@yaakapp/api';
|
||||||
|
|
||||||
export function migrateTemplateFunctionSelectOptions(
|
export function migrateTemplateFunctionSelectOptions(
|
||||||
f: TemplateFunctionPlugin,
|
f: TemplateFunctionPlugin,
|
||||||
@@ -13,8 +13,5 @@ export function migrateTemplateFunctionSelectOptions(
|
|||||||
return a;
|
return a;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { ...f, args: migratedArgs };
|
||||||
...f,
|
|
||||||
args: migratedArgs,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
150
packages/plugin-runtime/tests/common.test.ts
Normal file
150
packages/plugin-runtime/tests/common.test.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins';
|
||||||
|
import { Context, DynamicTemplateFunctionArg } from '@yaakapp/api';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { applyDynamicFormInput, applyFormInputDefaults } from '../src/common';
|
||||||
|
|
||||||
|
describe('applyFormInputDefaults', () => {
|
||||||
|
test('Works with top-level select', () => {
|
||||||
|
const args: DynamicTemplateFunctionArg[] = [
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'test',
|
||||||
|
options: [{ label: 'Option 1', value: 'one' }],
|
||||||
|
defaultValue: 'one',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(applyFormInputDefaults(args, {})).toEqual({
|
||||||
|
test: 'one',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Works with existing value', () => {
|
||||||
|
const args: DynamicTemplateFunctionArg[] = [
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'test',
|
||||||
|
options: [{ label: 'Option 1', value: 'one' }],
|
||||||
|
defaultValue: 'one',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(applyFormInputDefaults(args, { test: 'explicit' })).toEqual({
|
||||||
|
test: 'explicit',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Works with recursive select', () => {
|
||||||
|
const args: DynamicTemplateFunctionArg[] = [
|
||||||
|
{ type: 'text', name: 'dummy', defaultValue: 'top' },
|
||||||
|
{
|
||||||
|
type: 'accordion',
|
||||||
|
label: 'Test',
|
||||||
|
inputs: [
|
||||||
|
{ type: 'text', name: 'name', defaultValue: 'hello' },
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'test',
|
||||||
|
options: [{ label: 'Option 1', value: 'one' }],
|
||||||
|
defaultValue: 'one',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(applyFormInputDefaults(args, {})).toEqual({
|
||||||
|
dummy: 'top',
|
||||||
|
test: 'one',
|
||||||
|
name: 'hello',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Works with dynamic options', () => {
|
||||||
|
const args: DynamicTemplateFunctionArg[] = [
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'test',
|
||||||
|
defaultValue: 'one',
|
||||||
|
options: [],
|
||||||
|
dynamic() {
|
||||||
|
return { options: [{ label: 'Option 1', value: 'one' }] };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(applyFormInputDefaults(args, {})).toEqual({
|
||||||
|
test: 'one',
|
||||||
|
});
|
||||||
|
expect(applyFormInputDefaults(args, {})).toEqual({
|
||||||
|
test: 'one',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('applyDynamicFormInput', () => {
|
||||||
|
test('Works with plain input', async () => {
|
||||||
|
const ctx = {} as Context;
|
||||||
|
const args: DynamicTemplateFunctionArg[] = [
|
||||||
|
{ type: 'text', name: 'name' },
|
||||||
|
{ type: 'checkbox', name: 'checked' },
|
||||||
|
];
|
||||||
|
const callArgs: CallTemplateFunctionArgs = {
|
||||||
|
values: {},
|
||||||
|
purpose: 'preview',
|
||||||
|
};
|
||||||
|
expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([
|
||||||
|
{ type: 'text', name: 'name' },
|
||||||
|
{ type: 'checkbox', name: 'checked' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Works with dynamic input', async () => {
|
||||||
|
const ctx = {} as Context;
|
||||||
|
const args: DynamicTemplateFunctionArg[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'name',
|
||||||
|
async dynamic(_ctx, _args) {
|
||||||
|
return { hidden: true };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const callArgs: CallTemplateFunctionArgs = {
|
||||||
|
values: {},
|
||||||
|
purpose: 'preview',
|
||||||
|
};
|
||||||
|
expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([
|
||||||
|
{ type: 'text', name: 'name', hidden: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Works with recursive dynamic input', async () => {
|
||||||
|
const ctx = {} as Context;
|
||||||
|
const callArgs: CallTemplateFunctionArgs = {
|
||||||
|
values: { hello: 'world' },
|
||||||
|
purpose: 'preview',
|
||||||
|
};
|
||||||
|
const args: DynamicTemplateFunctionArg[] = [
|
||||||
|
{
|
||||||
|
type: 'banner',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'name',
|
||||||
|
async dynamic(_ctx, args) {
|
||||||
|
return { hidden: args.values.hello === 'world' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'banner',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'name',
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -57,10 +57,6 @@ export const plugin: PluginDefinition = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.method !== 'GET') {
|
|
||||||
headers['x-amz-content-sha256'] = 'UNSIGNED-PAYLOAD';
|
|
||||||
}
|
|
||||||
|
|
||||||
const signature = aws4.sign(
|
const signature = aws4.sign(
|
||||||
{
|
{
|
||||||
host: url.host,
|
host: url.host,
|
||||||
@@ -68,6 +64,7 @@ export const plugin: PluginDefinition = {
|
|||||||
path: url.pathname + (url.search || ''),
|
path: url.pathname + (url.search || ''),
|
||||||
service: String(values.service || 'sts'),
|
service: String(values.service || 'sts'),
|
||||||
region: values.region ? String(values.region) : undefined,
|
region: values.region ? String(values.region) : undefined,
|
||||||
|
body: values.body ? String(values.body) : undefined,
|
||||||
headers,
|
headers,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -77,11 +74,6 @@ export const plugin: PluginDefinition = {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// After signing, aws4 will set:
|
|
||||||
// - opts.headers["Authorization"]
|
|
||||||
// - opts.headers["X-Amz-Date"]
|
|
||||||
// - optionally content sha256 header etc
|
|
||||||
|
|
||||||
if (signature.headers == null) {
|
if (signature.headers == null) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ export const plugin: PluginDefinition = {
|
|||||||
{
|
{
|
||||||
type: 'accordion',
|
type: 'accordion',
|
||||||
label: 'Access Token Response',
|
label: 'Access Token Response',
|
||||||
|
inputs: [],
|
||||||
async dynamic(ctx, { contextId, values }) {
|
async dynamic(ctx, { contextId, values }) {
|
||||||
const tokenArgs: TokenStoreArgs = {
|
const tokenArgs: TokenStoreArgs = {
|
||||||
contextId,
|
contextId,
|
||||||
@@ -304,6 +305,7 @@ export const plugin: PluginDefinition = {
|
|||||||
inputs: [
|
inputs: [
|
||||||
{
|
{
|
||||||
type: 'editor',
|
type: 'editor',
|
||||||
|
name: 'response',
|
||||||
defaultValue: JSON.stringify(token.response, null, 2),
|
defaultValue: JSON.stringify(token.response, null, 2),
|
||||||
hideLabel: true,
|
hideLabel: true,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
|
||||||
|
const UTF8 = 'utf8';
|
||||||
const options = [
|
const options = [
|
||||||
{ label: 'ASCII', value: 'ascii' },
|
{ label: 'ASCII', value: 'ascii' },
|
||||||
{ label: 'UTF-8', value: 'utf8' },
|
{ label: 'UTF-8', value: UTF8 },
|
||||||
{ label: 'UTF-16 LE', value: 'utf16le' },
|
{ label: 'UTF-16 LE', value: 'utf16le' },
|
||||||
{ label: 'Base64', value: 'base64' },
|
{ label: 'Base64', value: 'base64' },
|
||||||
{ label: 'Base64 URL-safe', value: 'base64url' },
|
{ label: 'Base64 URL-safe', value: 'base64url' },
|
||||||
@@ -18,12 +19,11 @@ export const plugin: PluginDefinition = {
|
|||||||
args: [
|
args: [
|
||||||
{ title: 'Select File', type: 'file', name: 'path', label: 'File' },
|
{ title: 'Select File', type: 'file', name: 'path', label: 'File' },
|
||||||
{
|
{
|
||||||
title: 'Select encoding',
|
|
||||||
type: 'select',
|
type: 'select',
|
||||||
name: 'encoding',
|
name: 'encoding',
|
||||||
label: 'Encoding',
|
label: 'Encoding',
|
||||||
defaultValue: 'utf8',
|
defaultValue: UTF8,
|
||||||
description: 'Specifies how the file’s bytes are decoded into text when read',
|
description: "Specifies how the file's bytes are decoded into text when read",
|
||||||
options,
|
options,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"description": "Template functions for working with JSON data",
|
"description": "Template functions for working with JSON data",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import type { XPathResult } from '@yaak/template-function-xml';
|
||||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||||
import { JSONPath } from 'jsonpath-plus';
|
import { JSONPath } from 'jsonpath-plus';
|
||||||
|
|
||||||
|
const RETURN_FIRST = 'first';
|
||||||
|
const RETURN_ALL = 'all';
|
||||||
|
const RETURN_JOIN = 'join';
|
||||||
|
|
||||||
export const plugin: PluginDefinition = {
|
export const plugin: PluginDefinition = {
|
||||||
templateFunctions: [
|
templateFunctions: [
|
||||||
{
|
{
|
||||||
@@ -8,32 +13,59 @@ export const plugin: PluginDefinition = {
|
|||||||
description: 'Filter JSON-formatted text using JSONPath syntax',
|
description: 'Filter JSON-formatted text using JSONPath syntax',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'editor',
|
||||||
name: 'input',
|
name: 'input',
|
||||||
label: 'Input',
|
label: 'Input',
|
||||||
multiLine: true,
|
language: 'json',
|
||||||
placeholder: '{ "foo": "bar" }',
|
placeholder: '{ "foo": "bar" }',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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: 'checkbox',
|
||||||
|
name: 'formatted',
|
||||||
|
label: 'Pretty Print',
|
||||||
|
description: 'Format the output as JSON',
|
||||||
|
dynamic(_ctx, args) {
|
||||||
|
return { hidden: args.values.result === RETURN_JOIN };
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: 'text', name: 'query', label: 'Query', placeholder: '$..foo' },
|
{ type: 'text', name: 'query', label: 'Query', placeholder: '$..foo' },
|
||||||
{ type: 'checkbox', name: 'formatted', label: 'Format Output' },
|
|
||||||
],
|
],
|
||||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(String(args.values.input));
|
console.log('formatted', args.values.formatted);
|
||||||
const query = String(args.values.query ?? '$').trim();
|
return filterJSONPath(
|
||||||
let filtered = JSONPath({ path: query, json: parsed });
|
String(args.values.input),
|
||||||
if (Array.isArray(filtered)) {
|
String(args.values.query),
|
||||||
filtered = filtered[0];
|
(args.values.result || RETURN_FIRST) as XPathResult,
|
||||||
}
|
args.values.join == null ? null : String(args.values.join),
|
||||||
if (typeof filtered === 'string') {
|
Boolean(args.values.formatted),
|
||||||
return filtered;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (args.values.formatted) {
|
|
||||||
return JSON.stringify(filtered, null, 2);
|
|
||||||
} else {
|
|
||||||
return JSON.stringify(filtered);
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -79,3 +111,41 @@ export const plugin: PluginDefinition = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type JSONPathResult = 'first' | 'join' | 'all';
|
||||||
|
|
||||||
|
export function filterJSONPath(
|
||||||
|
body: string,
|
||||||
|
path: string,
|
||||||
|
result: JSONPathResult,
|
||||||
|
join: string | null,
|
||||||
|
formatted: boolean = false,
|
||||||
|
): string {
|
||||||
|
const parsed = JSON.parse(body);
|
||||||
|
let items = JSONPath({ path, json: parsed });
|
||||||
|
|
||||||
|
if (items == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(items)) {
|
||||||
|
// Already good
|
||||||
|
} else if (result === 'first') {
|
||||||
|
items = items[0] ?? '';
|
||||||
|
} else if (result === 'join') {
|
||||||
|
items = items.map((i) => objToStr(i, false)).join(join ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return objToStr(items, formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
function objToStr(o: unknown, formatted: boolean = false): string {
|
||||||
|
if (
|
||||||
|
Object.prototype.toString.call(o) === '[object Array]' ||
|
||||||
|
Object.prototype.toString.call(o) === '[object Object]'
|
||||||
|
) {
|
||||||
|
return formatted ? JSON.stringify(o, null, 2) : JSON.stringify(o);
|
||||||
|
} else {
|
||||||
|
return String(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,11 +10,6 @@
|
|||||||
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsonpath-plus": "^10.3.0",
|
"@yaak/template-function-xml": "*"
|
||||||
"xpath": "^0.0.34",
|
|
||||||
"@xmldom/xmldom": "^0.9.8"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/jsonpath": "^0.2.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,53 @@
|
|||||||
import { DOMParser } from '@xmldom/xmldom';
|
import type { JSONPathResult } from '@yaak/template-function-json';
|
||||||
|
import { filterJSONPath } from '@yaak/template-function-json';
|
||||||
|
import type { XPathResult } from '@yaak/template-function-xml';
|
||||||
|
import { filterXPath } from '@yaak/template-function-xml';
|
||||||
import type {
|
import type {
|
||||||
CallTemplateFunctionArgs,
|
CallTemplateFunctionArgs,
|
||||||
Context,
|
Context,
|
||||||
|
DynamicTemplateFunctionArg,
|
||||||
FormInput,
|
FormInput,
|
||||||
GetHttpAuthenticationConfigRequest,
|
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
PluginDefinition,
|
PluginDefinition,
|
||||||
RenderPurpose,
|
RenderPurpose,
|
||||||
} from '@yaakapp/api';
|
} from '@yaakapp/api';
|
||||||
import type { DynamicTemplateFunctionArg } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
|
|
||||||
import { JSONPath } from 'jsonpath-plus';
|
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import xpath from 'xpath';
|
|
||||||
|
|
||||||
const BEHAVIOR_TTL = 'ttl';
|
const BEHAVIOR_TTL = 'ttl';
|
||||||
const BEHAVIOR_ALWAYS = 'always';
|
const BEHAVIOR_ALWAYS = 'always';
|
||||||
const BEHAVIOR_SMART = 'smart';
|
const BEHAVIOR_SMART = 'smart';
|
||||||
|
|
||||||
const behaviorArg: FormInput = {
|
const RETURN_FIRST = 'first';
|
||||||
type: 'select',
|
const RETURN_ALL = 'all';
|
||||||
name: 'behavior',
|
const RETURN_JOIN = 'join';
|
||||||
label: 'Sending Behavior',
|
|
||||||
defaultValue: 'smart',
|
|
||||||
options: [
|
|
||||||
{ label: 'When no responses', value: BEHAVIOR_SMART },
|
|
||||||
{ label: 'Always', value: BEHAVIOR_ALWAYS },
|
|
||||||
{ label: 'When expired', value: BEHAVIOR_TTL },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const ttlArg: DynamicTemplateFunctionArg = {
|
const behaviorArgs: DynamicTemplateFunctionArg = {
|
||||||
type: 'text',
|
type: 'h_stack',
|
||||||
name: 'ttl',
|
inputs: [
|
||||||
label: 'Expiration Time (seconds)',
|
{
|
||||||
placeholder: '0',
|
type: 'select',
|
||||||
description: 'Resend the request when the latest response is older than this many seconds, or if there are no responses yet.',
|
name: 'behavior',
|
||||||
dynamic(_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) {
|
label: 'Sending Behavior',
|
||||||
const show = values.behavior === BEHAVIOR_TTL;
|
defaultValue: BEHAVIOR_SMART,
|
||||||
return { hidden: !show };
|
options: [
|
||||||
},
|
{ label: 'When no responses', value: BEHAVIOR_SMART },
|
||||||
|
{ label: 'Always', value: BEHAVIOR_ALWAYS },
|
||||||
|
{ label: 'When expired', value: BEHAVIOR_TTL },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'ttl',
|
||||||
|
label: 'TTL (seconds)',
|
||||||
|
placeholder: '0',
|
||||||
|
defaultValue: '0',
|
||||||
|
description:
|
||||||
|
'Resend the request when the latest response is older than this many seconds, or if there are no responses yet. "0" means never expires',
|
||||||
|
dynamic(_ctx, args) {
|
||||||
|
return { hidden: args.values.behavior !== BEHAVIOR_TTL };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestArg: FormInput = {
|
const requestArg: FormInput = {
|
||||||
@@ -54,14 +63,13 @@ export const plugin: PluginDefinition = {
|
|||||||
description: 'Read the value of a response header, by name',
|
description: 'Read the value of a response header, by name',
|
||||||
args: [
|
args: [
|
||||||
requestArg,
|
requestArg,
|
||||||
|
behaviorArgs,
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'header',
|
name: 'header',
|
||||||
label: 'Header Name',
|
label: 'Header Name',
|
||||||
placeholder: 'Content-Type',
|
placeholder: 'Content-Type',
|
||||||
},
|
},
|
||||||
behaviorArg,
|
|
||||||
ttlArg,
|
|
||||||
],
|
],
|
||||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||||
if (!args.values.request || !args.values.header) return null;
|
if (!args.values.request || !args.values.header) return null;
|
||||||
@@ -86,14 +94,67 @@ export const plugin: PluginDefinition = {
|
|||||||
aliases: ['response'],
|
aliases: ['response'],
|
||||||
args: [
|
args: [
|
||||||
requestArg,
|
requestArg,
|
||||||
|
behaviorArgs,
|
||||||
|
{
|
||||||
|
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',
|
type: 'text',
|
||||||
name: 'path',
|
name: 'path',
|
||||||
label: 'JSONPath or XPath',
|
label: 'JSONPath or XPath',
|
||||||
placeholder: '$.books[0].id or /books[0]/id',
|
placeholder: '$.books[0].id or /books[0]/id',
|
||||||
|
dynamic: async (ctx, args) => {
|
||||||
|
const resp = await getResponse(ctx, {
|
||||||
|
requestId: String(args.values.request || ''),
|
||||||
|
purpose: 'preview',
|
||||||
|
behavior: args.values.behavior ? String(args.values.behavior) : null,
|
||||||
|
ttl: String(args.values.ttl || ''),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType =
|
||||||
|
resp?.headers.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? '';
|
||||||
|
if (contentType.includes('xml') || contentType?.includes('html')) {
|
||||||
|
return {
|
||||||
|
label: 'XPath',
|
||||||
|
placeholder: '/books[0]/id',
|
||||||
|
description: 'Enter an XPath expression used to filter the results',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
label: 'JSONPath',
|
||||||
|
placeholder: '$.books[0].id',
|
||||||
|
description: 'Enter a JSONPath expression used to filter the results',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
behaviorArg,
|
|
||||||
ttlArg,
|
|
||||||
],
|
],
|
||||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||||
if (!args.values.request || !args.values.path) return null;
|
if (!args.values.request || !args.values.path) return null;
|
||||||
@@ -118,13 +179,35 @@ export const plugin: PluginDefinition = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return filterJSONPath(body, String(args.values.path || ''));
|
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 {
|
} catch {
|
||||||
// Probably not JSON, try XPath
|
// Probably not JSON, try XPath
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return filterXPath(body, String(args.values.path || ''));
|
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 {
|
} catch {
|
||||||
// Probably not XML
|
// Probably not XML
|
||||||
}
|
}
|
||||||
@@ -136,7 +219,7 @@ export const plugin: PluginDefinition = {
|
|||||||
name: 'response.body.raw',
|
name: 'response.body.raw',
|
||||||
description: 'Access the entire response body, as text',
|
description: 'Access the entire response body, as text',
|
||||||
aliases: ['response'],
|
aliases: ['response'],
|
||||||
args: [requestArg, behaviorArg, ttlArg],
|
args: [requestArg, behaviorArgs],
|
||||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||||
if (!args.values.request) return null;
|
if (!args.values.request) return null;
|
||||||
|
|
||||||
@@ -165,36 +248,6 @@ export const plugin: PluginDefinition = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
function filterJSONPath(body: string, path: string): string {
|
|
||||||
const parsed = JSON.parse(body);
|
|
||||||
const items = JSONPath({ path, json: parsed })[0];
|
|
||||||
if (items == null) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
Object.prototype.toString.call(items) === '[object Array]' ||
|
|
||||||
Object.prototype.toString.call(items) === '[object Object]'
|
|
||||||
) {
|
|
||||||
return JSON.stringify(items);
|
|
||||||
} else {
|
|
||||||
return String(items);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterXPath(body: string, path: string): string {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const doc: any = new DOMParser().parseFromString(body, 'text/xml');
|
|
||||||
const items = xpath.select(path, doc, false);
|
|
||||||
|
|
||||||
if (Array.isArray(items)) {
|
|
||||||
return items[0] != null ? String(items[0].firstChild ?? '') : '';
|
|
||||||
} else {
|
|
||||||
// Not sure what cases this happens in (?)
|
|
||||||
return String(items);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getResponse(
|
async function getResponse(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
{
|
{
|
||||||
@@ -244,8 +297,8 @@ async function getResponse(
|
|||||||
|
|
||||||
function shouldSendExpired(response: HttpResponse | null, ttl: string | null): boolean {
|
function shouldSendExpired(response: HttpResponse | null, ttl: string | null): boolean {
|
||||||
if (response == null) return true;
|
if (response == null) return true;
|
||||||
const ttlSeconds = parseInt(ttl || '0');
|
const ttlSeconds = parseInt(ttl || '0') || 0;
|
||||||
if (isNaN(ttlSeconds)) throw new Error(`Invalid TTL "${ttl}"`);
|
if (ttlSeconds === 0) return false;
|
||||||
const nowMillis = Date.now();
|
const nowMillis = Date.now();
|
||||||
const respMillis = new Date(response.createdAt + 'Z').getTime();
|
const respMillis = new Date(response.createdAt + 'Z').getTime();
|
||||||
return respMillis + ttlSeconds * 1000 < nowMillis;
|
return respMillis + ttlSeconds * 1000 < nowMillis;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"description": "Template functions for working with XML data",
|
"description": "Template functions for working with XML data",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev",
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { DOMParser } from '@xmldom/xmldom';
|
|||||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||||
import xpath from 'xpath';
|
import xpath from 'xpath';
|
||||||
|
|
||||||
|
const RETURN_FIRST = 'first';
|
||||||
|
const RETURN_ALL = 'all';
|
||||||
|
const RETURN_JOIN = 'join';
|
||||||
|
|
||||||
export const plugin: PluginDefinition = {
|
export const plugin: PluginDefinition = {
|
||||||
templateFunctions: [
|
templateFunctions: [
|
||||||
{
|
{
|
||||||
@@ -15,20 +19,39 @@ export const plugin: PluginDefinition = {
|
|||||||
multiLine: true,
|
multiLine: true,
|
||||||
placeholder: '<foo></foo>',
|
placeholder: '<foo></foo>',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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: 'query', label: 'Query', placeholder: '//foo' },
|
{ type: 'text', name: 'query', label: 'Query', placeholder: '//foo' },
|
||||||
],
|
],
|
||||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const result = (args.values.result || RETURN_FIRST) as XPathResult;
|
||||||
const doc: any = new DOMParser().parseFromString(String(args.values.input), 'text/xml');
|
const join = args.values.join == null ? null : String(args.values.join);
|
||||||
const result = xpath.select(String(args.values.query), doc, false);
|
return filterXPath(String(args.values.input), String(args.values.query), result, join);
|
||||||
if (Array.isArray(result)) {
|
|
||||||
return String(result.map((c) => String(c.firstChild))[0] ?? '');
|
|
||||||
} else if (result instanceof Node) {
|
|
||||||
return String(result.firstChild);
|
|
||||||
} else {
|
|
||||||
return String(result);
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -36,3 +59,26 @@ export const plugin: PluginDefinition = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type XPathResult = 'first' | 'join' | 'all';
|
||||||
|
export function filterXPath(
|
||||||
|
body: string,
|
||||||
|
path: string,
|
||||||
|
result: XPathResult,
|
||||||
|
join: string | null,
|
||||||
|
): string {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const doc: any = new DOMParser().parseFromString(body, 'text/xml');
|
||||||
|
const items = xpath.select(path, doc, false);
|
||||||
|
|
||||||
|
if (!Array.isArray(items)) {
|
||||||
|
return String(items);
|
||||||
|
} else if (!Array.isArray(items) || result === 'first') {
|
||||||
|
return items[0] != null ? String(items[0].firstChild ?? '') : '';
|
||||||
|
} else if (result === 'join') {
|
||||||
|
return items.map((item) => String(item.firstChild ?? '')).join(join ?? '');
|
||||||
|
} else {
|
||||||
|
// Not sure what cases this happens in (?)
|
||||||
|
return String(items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
event: &InternalEvent,
|
event: &InternalEvent,
|
||||||
plugin_handle: &PluginHandle,
|
plugin_handle: &PluginHandle,
|
||||||
) -> Result<Option<InternalEventPayload>> {
|
) -> Result<Option<InternalEventPayload>> {
|
||||||
// debug!("Got event to app {event:?}");
|
// log::debug!("Got event to app {event:?}");
|
||||||
let plugin_context = event.context.to_owned();
|
let plugin_context = event.context.to_owned();
|
||||||
match event.clone().payload {
|
match event.clone().payload {
|
||||||
InternalEventPayload::CopyTextRequest(req) => {
|
InternalEventPayload::CopyTextRequest(req) => {
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ use crate::error::Error::ModelNotFound;
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::{AnyModel, UpsertModelInfo};
|
use crate::models::{AnyModel, UpsertModelInfo};
|
||||||
use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource};
|
use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource};
|
||||||
use log::{error, warn};
|
use log::error;
|
||||||
use rusqlite::OptionalExtension;
|
use rusqlite::OptionalExtension;
|
||||||
use sea_query::{
|
use sea_query::{
|
||||||
Alias, Asterisk, Expr, Func, IntoColumnRef, IntoIden, IntoTableRef, OnConflict, Query,
|
Asterisk, Expr, Func, IntoColumnRef, IntoIden, IntoTableRef, OnConflict, Query, SimpleExpr,
|
||||||
ReturningClause, SimpleExpr, SqliteQueryBuilder,
|
SqliteQueryBuilder,
|
||||||
};
|
};
|
||||||
use sea_query_rusqlite::RusqliteBinder;
|
use sea_query_rusqlite::RusqliteBinder;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::commands::*;
|
use crate::commands::*;
|
||||||
use crate::migrate::migrate_db;
|
use crate::migrate::migrate_db;
|
||||||
use crate::query_manager::QueryManager;
|
use crate::query_manager::QueryManager;
|
||||||
use crate::util::ModelChangeEvent;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use r2d2::Pool;
|
use r2d2::Pool;
|
||||||
use r2d2_sqlite::SqliteConnectionManager;
|
use r2d2_sqlite::SqliteConnectionManager;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
|
|||||||
|
|
||||||
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
|
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
|
||||||
|
|
||||||
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: JsonValue }, };
|
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: JsonPrimitive }, };
|
||||||
|
|
||||||
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
|
|||||||
|
|
||||||
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
|
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
|
||||||
|
|
||||||
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
|
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
|
||||||
|
|
||||||
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
|
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
|
||||||
|
|
||||||
@@ -224,6 +224,8 @@ defaultValue?: string, disabled?: boolean,
|
|||||||
*/
|
*/
|
||||||
description?: string, };
|
description?: string, };
|
||||||
|
|
||||||
|
export type FormInputHStack = { inputs?: Array<FormInput>, };
|
||||||
|
|
||||||
export type FormInputHttpRequest = {
|
export type FormInputHttpRequest = {
|
||||||
/**
|
/**
|
||||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
|
|||||||
@@ -658,6 +658,18 @@ pub enum JsonPrimitive {
|
|||||||
Null,
|
Null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Value> for JsonPrimitive {
|
||||||
|
fn from(value: serde_json::Value) -> Self {
|
||||||
|
match value {
|
||||||
|
serde_json::Value::Null => JsonPrimitive::Null,
|
||||||
|
serde_json::Value::Bool(b) => JsonPrimitive::Boolean(b),
|
||||||
|
serde_json::Value::Number(n) => JsonPrimitive::Number(n.as_f64().unwrap()),
|
||||||
|
serde_json::Value::String(s) => JsonPrimitive::String(s),
|
||||||
|
v => panic!("Unsupported JSON primitive type {:?}", v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_events.ts")]
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
@@ -733,6 +745,7 @@ pub enum FormInput {
|
|||||||
File(FormInputFile),
|
File(FormInputFile),
|
||||||
HttpRequest(FormInputHttpRequest),
|
HttpRequest(FormInputHttpRequest),
|
||||||
Accordion(FormInputAccordion),
|
Accordion(FormInputAccordion),
|
||||||
|
HStack(FormInputHStack),
|
||||||
Banner(FormInputBanner),
|
Banner(FormInputBanner),
|
||||||
Markdown(FormInputMarkdown),
|
Markdown(FormInputMarkdown),
|
||||||
}
|
}
|
||||||
@@ -895,7 +908,7 @@ pub struct FormInputFile {
|
|||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
pub directory: Option<bool>,
|
pub directory: Option<bool>,
|
||||||
|
|
||||||
// Default file path for selection dialog
|
// Default file path for the selection dialog
|
||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
pub default_path: Option<String>,
|
pub default_path: Option<String>,
|
||||||
|
|
||||||
@@ -953,6 +966,14 @@ pub struct FormInputAccordion {
|
|||||||
pub hidden: Option<bool>,
|
pub hidden: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(default, rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
|
pub struct FormInputHStack {
|
||||||
|
#[ts(optional)]
|
||||||
|
pub inputs: Option<Vec<FormInput>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_events.ts")]
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
@@ -1015,7 +1036,7 @@ pub struct CallTemplateFunctionResponse {
|
|||||||
#[ts(export, export_to = "gen_events.ts")]
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
pub struct CallTemplateFunctionArgs {
|
pub struct CallTemplateFunctionArgs {
|
||||||
pub purpose: RenderPurpose,
|
pub purpose: RenderPurpose,
|
||||||
pub values: HashMap<String, serde_json::Value>,
|
pub values: HashMap<String, JsonPrimitive>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
|
|||||||
@@ -786,7 +786,7 @@ impl PluginManager {
|
|||||||
&self,
|
&self,
|
||||||
plugin_context: &PluginContext,
|
plugin_context: &PluginContext,
|
||||||
fn_name: &str,
|
fn_name: &str,
|
||||||
values: HashMap<String, serde_json::Value>,
|
values: HashMap<String, JsonPrimitive>,
|
||||||
purpose: RenderPurpose,
|
purpose: RenderPurpose,
|
||||||
) -> TemplateResult<String> {
|
) -> TemplateResult<String> {
|
||||||
let req = CallTemplateFunctionRequest {
|
let req = CallTemplateFunctionRequest {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::events::{PluginContext, RenderPurpose};
|
use crate::events::{JsonPrimitive, PluginContext, RenderPurpose};
|
||||||
use crate::manager::PluginManager;
|
use crate::manager::PluginManager;
|
||||||
use crate::native_template_functions::{
|
use crate::native_template_functions::{
|
||||||
template_function_keychain_run, template_function_secure_run,
|
template_function_keychain_run, template_function_secure_run,
|
||||||
@@ -42,12 +42,17 @@ impl<R: Runtime> TemplateCallback for PluginTemplateCallback<R> {
|
|||||||
return template_function_keychain_run(args);
|
return template_function_keychain_run(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut primitive_args = HashMap::new();
|
||||||
|
for (key, value) in args {
|
||||||
|
primitive_args.insert(key, JsonPrimitive::from(value));
|
||||||
|
}
|
||||||
|
|
||||||
let plugin_manager = &*self.app_handle.state::<PluginManager>();
|
let plugin_manager = &*self.app_handle.state::<PluginManager>();
|
||||||
let resp = plugin_manager
|
let resp = plugin_manager
|
||||||
.call_template_function(
|
.call_template_function(
|
||||||
&self.plugin_context,
|
&self.plugin_context,
|
||||||
fn_name,
|
fn_name,
|
||||||
args,
|
primitive_args,
|
||||||
self.render_purpose.to_owned(),
|
self.render_purpose.to_owned(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { IconButton } from './core/IconButton';
|
|||||||
import { Input } from './core/Input';
|
import { Input } from './core/Input';
|
||||||
import { Label } from './core/Label';
|
import { Label } from './core/Label';
|
||||||
import { Select } from './core/Select';
|
import { Select } from './core/Select';
|
||||||
import { VStack } from './core/Stacks';
|
import { HStack, VStack } from './core/Stacks';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
import { SelectFile } from './SelectFile';
|
import { SelectFile } from './SelectFile';
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ interface Props<T> {
|
|||||||
autocompleteFunctions?: boolean;
|
autocompleteFunctions?: boolean;
|
||||||
autocompleteVariables?: boolean;
|
autocompleteVariables?: boolean;
|
||||||
stateKey: string;
|
stateKey: string;
|
||||||
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
|||||||
autocompleteVariables,
|
autocompleteVariables,
|
||||||
autocompleteFunctions,
|
autocompleteFunctions,
|
||||||
stateKey,
|
stateKey,
|
||||||
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
const setDataAttr = useCallback(
|
const setDataAttr = useCallback(
|
||||||
@@ -61,7 +63,7 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormInputs
|
<FormInputsStack
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
inputs={inputs}
|
inputs={inputs}
|
||||||
setDataAttr={setDataAttr}
|
setDataAttr={setDataAttr}
|
||||||
@@ -69,28 +71,15 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
|||||||
autocompleteFunctions={autocompleteFunctions}
|
autocompleteFunctions={autocompleteFunctions}
|
||||||
autocompleteVariables={autocompleteVariables}
|
autocompleteVariables={autocompleteVariables}
|
||||||
data={data}
|
data={data}
|
||||||
className="pb-4" // Pad the bottom to look nice
|
className={classNames(className, 'pb-4')} // Pad the bottom to look nice
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormInputs<T extends Record<string, JsonPrimitive>>({
|
function FormInputsStack<T extends Record<string, JsonPrimitive>>({
|
||||||
inputs,
|
|
||||||
autocompleteFunctions,
|
|
||||||
autocompleteVariables,
|
|
||||||
stateKey,
|
|
||||||
setDataAttr,
|
|
||||||
data,
|
|
||||||
disabled,
|
|
||||||
className,
|
className,
|
||||||
}: Pick<
|
...props
|
||||||
Props<T>,
|
}: FormInputsProps<T> & { className?: string }) {
|
||||||
'inputs' | 'autocompleteFunctions' | 'autocompleteVariables' | 'stateKey' | 'data'
|
|
||||||
> & {
|
|
||||||
setDataAttr: (name: string, value: JsonPrimitive) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<VStack
|
<VStack
|
||||||
space={3}
|
space={3}
|
||||||
@@ -100,6 +89,30 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
'pr-1', // A bit of space between inputs and scrollbar
|
'pr-1', // A bit of space between inputs and scrollbar
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<FormInputs {...props} />
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormInputsProps<T> = Pick<
|
||||||
|
Props<T>,
|
||||||
|
'inputs' | 'autocompleteFunctions' | 'autocompleteVariables' | 'stateKey' | 'data'
|
||||||
|
> & {
|
||||||
|
setDataAttr: (name: string, value: JsonPrimitive) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||||
|
inputs,
|
||||||
|
autocompleteFunctions,
|
||||||
|
autocompleteVariables,
|
||||||
|
stateKey,
|
||||||
|
setDataAttr,
|
||||||
|
data,
|
||||||
|
disabled,
|
||||||
|
}: FormInputsProps<T>) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
{inputs?.map((input, i) => {
|
{inputs?.map((input, i) => {
|
||||||
if ('hidden' in input && input.hidden) {
|
if ('hidden' in input && input.hidden) {
|
||||||
return null;
|
return null;
|
||||||
@@ -113,7 +126,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
case 'select':
|
case 'select':
|
||||||
return (
|
return (
|
||||||
<SelectArg
|
<SelectArg
|
||||||
key={i + stateKey}
|
key={i}
|
||||||
arg={input}
|
arg={input}
|
||||||
onChange={(v) => setDataAttr(input.name, v)}
|
onChange={(v) => setDataAttr(input.name, v)}
|
||||||
value={
|
value={
|
||||||
@@ -126,7 +139,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
case 'text':
|
case 'text':
|
||||||
return (
|
return (
|
||||||
<TextArg
|
<TextArg
|
||||||
key={i}
|
key={i + stateKey}
|
||||||
stateKey={stateKey}
|
stateKey={stateKey}
|
||||||
arg={input}
|
arg={input}
|
||||||
autocompleteFunctions={autocompleteFunctions || false}
|
autocompleteFunctions={autocompleteFunctions || false}
|
||||||
@@ -140,7 +153,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
case 'editor':
|
case 'editor':
|
||||||
return (
|
return (
|
||||||
<EditorArg
|
<EditorArg
|
||||||
key={i}
|
key={i + stateKey}
|
||||||
stateKey={stateKey}
|
stateKey={stateKey}
|
||||||
arg={input}
|
arg={input}
|
||||||
autocompleteFunctions={autocompleteFunctions || false}
|
autocompleteFunctions={autocompleteFunctions || false}
|
||||||
@@ -182,13 +195,13 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
);
|
);
|
||||||
case 'accordion':
|
case 'accordion':
|
||||||
return (
|
return (
|
||||||
<div key={i}>
|
<div key={i + stateKey}>
|
||||||
<DetailsBanner
|
<DetailsBanner
|
||||||
summary={input.label}
|
summary={input.label}
|
||||||
className={classNames('!mb-auto', disabled && 'opacity-disabled')}
|
className={classNames('!mb-auto', disabled && 'opacity-disabled')}
|
||||||
>
|
>
|
||||||
<div className="my-3">
|
<div className="my-3">
|
||||||
<FormInputs
|
<FormInputsStack
|
||||||
data={data}
|
data={data}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
inputs={input.inputs}
|
inputs={input.inputs}
|
||||||
@@ -201,14 +214,28 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
</DetailsBanner>
|
</DetailsBanner>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
case 'h_stack':
|
||||||
|
return (
|
||||||
|
<HStack key={i + stateKey} alignItems="end" space={3}>
|
||||||
|
<FormInputs
|
||||||
|
data={data}
|
||||||
|
disabled={disabled}
|
||||||
|
inputs={input.inputs}
|
||||||
|
setDataAttr={setDataAttr}
|
||||||
|
stateKey={stateKey}
|
||||||
|
autocompleteFunctions={autocompleteFunctions || false}
|
||||||
|
autocompleteVariables={autocompleteVariables}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
case 'banner':
|
case 'banner':
|
||||||
return (
|
return (
|
||||||
<Banner
|
<Banner
|
||||||
key={i}
|
key={i + stateKey}
|
||||||
color={input.color}
|
color={input.color}
|
||||||
className={classNames(disabled && 'opacity-disabled')}
|
className={classNames(disabled && 'opacity-disabled')}
|
||||||
>
|
>
|
||||||
<FormInputs
|
<FormInputsStack
|
||||||
data={data}
|
data={data}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
inputs={input.inputs}
|
inputs={input.inputs}
|
||||||
@@ -220,10 +247,10 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
</Banner>
|
</Banner>
|
||||||
);
|
);
|
||||||
case 'markdown':
|
case 'markdown':
|
||||||
return <Markdown>{input.content}</Markdown>;
|
return <Markdown key={i + stateKey}>{input.content}</Markdown>;
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</VStack>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +282,7 @@ function TextArg({
|
|||||||
type={arg.password ? 'password' : 'text'}
|
type={arg.password ? 'password' : 'text'}
|
||||||
label={arg.label ?? arg.name}
|
label={arg.label ?? arg.name}
|
||||||
size={INPUT_SIZE}
|
size={INPUT_SIZE}
|
||||||
hideLabel={arg.label == null}
|
hideLabel={arg.hideLabel ?? arg.label == null}
|
||||||
placeholder={arg.placeholder ?? undefined}
|
placeholder={arg.placeholder ?? undefined}
|
||||||
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
|
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
|
||||||
autocompleteFunctions={autocompleteFunctions}
|
autocompleteFunctions={autocompleteFunctions}
|
||||||
@@ -313,7 +340,9 @@ function EditorArg({
|
|||||||
language={arg.language}
|
language={arg.language}
|
||||||
readOnly={arg.readOnly}
|
readOnly={arg.readOnly}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
hideGutter
|
||||||
heightMode="auto"
|
heightMode="auto"
|
||||||
|
className="min-h-[3rem]"
|
||||||
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
|
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
|
||||||
placeholder={arg.placeholder ?? undefined}
|
placeholder={arg.placeholder ?? undefined}
|
||||||
autocompleteFunctions={autocompleteFunctions}
|
autocompleteFunctions={autocompleteFunctions}
|
||||||
@@ -374,7 +403,6 @@ function EditorArg({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
hideGutter
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -396,6 +424,7 @@ function SelectArg({
|
|||||||
name={arg.name}
|
name={arg.name}
|
||||||
help={arg.description}
|
help={arg.description}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
defaultValue={arg.defaultValue}
|
||||||
hideLabel={arg.hideLabel}
|
hideLabel={arg.hideLabel}
|
||||||
value={value}
|
value={value}
|
||||||
size={INPUT_SIZE}
|
size={INPUT_SIZE}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type {
|
|||||||
WebsocketRequest,
|
WebsocketRequest,
|
||||||
Workspace,
|
Workspace,
|
||||||
} from '@yaakapp-internal/models';
|
} from '@yaakapp-internal/models';
|
||||||
import type { TemplateFunction } from '@yaakapp-internal/plugins';
|
import type { FormInput, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||||
import type { FnArg, Tokens } from '@yaakapp-internal/templates';
|
import type { FnArg, Tokens } from '@yaakapp-internal/templates';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
@@ -45,24 +45,7 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
(async function () {
|
(async function () {
|
||||||
const initial: Record<string, string> = {};
|
const initial = collectArgumentValues(initialTokens, templateFunction);
|
||||||
const initialArgs =
|
|
||||||
initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'fn'
|
|
||||||
? initialTokens.tokens[0]?.val.args
|
|
||||||
: [];
|
|
||||||
for (const arg of templateFunction.args) {
|
|
||||||
if (!('name' in arg)) {
|
|
||||||
// Skip visual-only args
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const initialArg = initialArgs.find((a) => a.name === arg.name);
|
|
||||||
const initialArgValue =
|
|
||||||
initialArg?.value.type === 'str'
|
|
||||||
? initialArg?.value.text
|
|
||||||
: // TODO: Implement variable-based args
|
|
||||||
undefined;
|
|
||||||
initial[arg.name] = initialArgValue ?? arg.defaultValue ?? DYNAMIC_FORM_NULL_ARG;
|
|
||||||
}
|
|
||||||
|
|
||||||
// HACK: Replace the secure() function's encrypted `value` arg with the decrypted version so
|
// HACK: Replace the secure() function's encrypted `value` arg with the decrypted version so
|
||||||
// we can display it in the editor input.
|
// we can display it in the editor input.
|
||||||
@@ -71,12 +54,14 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
|
|||||||
initial.value = await convertTemplateToInsecure(template);
|
initial.value = await convertTemplateToInsecure(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('INITIAL', initial);
|
||||||
setInitialArgValues(initial);
|
setInitialArgValues(initial);
|
||||||
})().catch(console.error);
|
})().catch(console.error);
|
||||||
}, [
|
}, [
|
||||||
initialArgValues,
|
initialArgValues,
|
||||||
initialTokens,
|
initialTokens,
|
||||||
initialTokens.tokens,
|
initialTokens.tokens,
|
||||||
|
templateFunction,
|
||||||
templateFunction.args,
|
templateFunction.args,
|
||||||
templateFunction.name,
|
templateFunction.name,
|
||||||
]);
|
]);
|
||||||
@@ -159,84 +144,117 @@ function InitializedTemplateFunctionDialog({
|
|||||||
if (templateFunction == null) return null;
|
if (templateFunction == null) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack
|
<form
|
||||||
as="form"
|
className="grid grid-rows-[minmax(0,1fr)_auto_auto] h-full max-h-[90vh]"
|
||||||
className="pb-3"
|
|
||||||
space={4}
|
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleDone();
|
handleDone();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name === 'secure' ? (
|
<div className="overflow-y-auto h-full px-6">
|
||||||
<PlainInput
|
{name === 'secure' ? (
|
||||||
required
|
<PlainInput
|
||||||
label="Value"
|
required
|
||||||
name="value"
|
label="Value"
|
||||||
type="password"
|
name="value"
|
||||||
placeholder="••••••••••••"
|
type="password"
|
||||||
defaultValue={String(argValues['value'] ?? '')}
|
placeholder="••••••••••••"
|
||||||
onChange={(value) => setArgValues({ ...argValues, value })}
|
defaultValue={String(argValues['value'] ?? '')}
|
||||||
/>
|
onChange={(value) => setArgValues({ ...argValues, value })}
|
||||||
) : (
|
/>
|
||||||
<DynamicForm
|
) : (
|
||||||
autocompleteVariables
|
<DynamicForm
|
||||||
autocompleteFunctions
|
autocompleteVariables
|
||||||
inputs={templateFunction.args}
|
autocompleteFunctions
|
||||||
data={argValues}
|
inputs={templateFunction.args}
|
||||||
onChange={setArgValues}
|
data={argValues}
|
||||||
stateKey={`template_function.${templateFunction.name}`}
|
onChange={setArgValues}
|
||||||
/>
|
stateKey={`template_function.${templateFunction.name}`}
|
||||||
)}
|
/>
|
||||||
{enablePreview && (
|
|
||||||
<VStack className="w-full" space={1}>
|
|
||||||
<HStack space={0.5}>
|
|
||||||
<HStack className="text-sm text-text-subtle" space={1.5}>
|
|
||||||
Rendered Preview
|
|
||||||
{rendered.isPending && <LoadingIcon size="xs" />}
|
|
||||||
</HStack>
|
|
||||||
<IconButton
|
|
||||||
size="xs"
|
|
||||||
iconSize="sm"
|
|
||||||
icon={showSecretsInPreview ? 'lock' : 'lock_open'}
|
|
||||||
title={showSecretsInPreview ? 'Show preview' : 'Hide preview'}
|
|
||||||
onClick={toggleShowSecretsInPreview}
|
|
||||||
className={classNames(
|
|
||||||
'ml-auto text-text-subtlest',
|
|
||||||
!dataContainsSecrets && 'invisible',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
<InlineCode
|
|
||||||
className={classNames(
|
|
||||||
'whitespace-pre-wrap !select-text cursor-text max-h-[10rem] overflow-y-auto hide-scrollbars',
|
|
||||||
tooLarge && 'italic text-danger',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{rendered.error || tagText.error ? (
|
|
||||||
<em className="text-danger">
|
|
||||||
{`${rendered.error || tagText.error}`.replace(/^Render Error: /, '')}
|
|
||||||
</em>
|
|
||||||
) : dataContainsSecrets && !showSecretsInPreview ? (
|
|
||||||
<span className="italic text-text-subtle">------ sensitive values hidden ------</span>
|
|
||||||
) : tooLarge ? (
|
|
||||||
'too large to preview'
|
|
||||||
) : (
|
|
||||||
rendered.data || <> </>
|
|
||||||
)}
|
|
||||||
</InlineCode>
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
<div className="flex justify-stretch w-full flex-grow gap-2 [&>*]:flex-1">
|
|
||||||
{templateFunction.name === 'secure' && (
|
|
||||||
<Button variant="border" color="secondary" onClick={setupOrConfigureEncryption}>
|
|
||||||
Reveal Encryption Key
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
<Button type="submit" color="primary">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</VStack>
|
<div className="px-6 border-t border-t-border py-3 bg-surface-highlight w-full flex flex-col gap-4">
|
||||||
|
{enablePreview ? (
|
||||||
|
<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" />}
|
||||||
|
</HStack>
|
||||||
|
<IconButton
|
||||||
|
size="xs"
|
||||||
|
iconSize="sm"
|
||||||
|
icon={showSecretsInPreview ? 'lock' : 'lock_open'}
|
||||||
|
title={showSecretsInPreview ? 'Show preview' : 'Hide preview'}
|
||||||
|
onClick={toggleShowSecretsInPreview}
|
||||||
|
className={classNames(
|
||||||
|
'ml-auto text-text-subtlest',
|
||||||
|
!dataContainsSecrets && 'invisible',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
<InlineCode
|
||||||
|
className={classNames(
|
||||||
|
'whitespace-pre-wrap !select-text cursor-text max-h-[10rem] overflow-y-auto hide-scrollbars !border-text-subtlest',
|
||||||
|
tooLarge && 'italic text-danger',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{rendered.error || tagText.error ? (
|
||||||
|
<em className="text-danger">
|
||||||
|
{`${rendered.error || tagText.error}`.replace(/^Render Error: /, '')}
|
||||||
|
</em>
|
||||||
|
) : dataContainsSecrets && !showSecretsInPreview ? (
|
||||||
|
<span className="italic text-text-subtle">
|
||||||
|
------ sensitive values hidden ------
|
||||||
|
</span>
|
||||||
|
) : tooLarge ? (
|
||||||
|
'too large to preview'
|
||||||
|
) : (
|
||||||
|
rendered.data || <> </>
|
||||||
|
)}
|
||||||
|
</InlineCode>
|
||||||
|
</VStack>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
<div className="flex justify-stretch w-full flex-grow gap-2 [&>*]:flex-1">
|
||||||
|
{templateFunction.name === 'secure' && (
|
||||||
|
<Button variant="border" color="secondary" onClick={setupOrConfigureEncryption}>
|
||||||
|
Reveal Encryption Key
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button type="submit" color="primary">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the initial tokens from the template and merge those with the default values pulled from
|
||||||
|
* the template function definition.
|
||||||
|
*/
|
||||||
|
function collectArgumentValues(initialTokens: Tokens, templateFunction: TemplateFunction) {
|
||||||
|
const initial: Record<string, string | boolean> = {};
|
||||||
|
const initialArgs =
|
||||||
|
initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'fn'
|
||||||
|
? initialTokens.tokens[0]?.val.args
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const processArg = (arg: FormInput) => {
|
||||||
|
if ('inputs' in arg && arg.inputs) {
|
||||||
|
arg.inputs.forEach(processArg);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
templateFunction.args.forEach(processArg);
|
||||||
|
|
||||||
|
return initial;
|
||||||
|
}
|
||||||
|
|||||||
@@ -655,7 +655,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
|||||||
className,
|
className,
|
||||||
'h-xs', // More compact
|
'h-xs', // More compact
|
||||||
'min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap',
|
'min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap',
|
||||||
'focus:bg-surface-highlight focus:text rounded',
|
'focus:bg-surface-highlight focus:text rounded focus:outline-none focus-visible:outline-1',
|
||||||
item.color === 'danger' && '!text-danger',
|
item.color === 'danger' && '!text-danger',
|
||||||
item.color === 'primary' && '!text-primary',
|
item.color === 'primary' && '!text-primary',
|
||||||
item.color === 'success' && '!text-success',
|
item.color === 'success' && '!text-success',
|
||||||
|
|||||||
@@ -78,9 +78,19 @@
|
|||||||
@apply cursor-default;
|
@apply cursor-default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-gutter-lint {
|
||||||
|
@apply w-auto !important;
|
||||||
|
|
||||||
|
.cm-gutterElement {
|
||||||
|
@apply px-0;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-lint-marker {
|
.cm-lint-marker {
|
||||||
@apply cursor-default opacity-80 hover:opacity-100 transition-opacity;
|
@apply cursor-default opacity-80 hover:opacity-100 transition-opacity;
|
||||||
@apply rounded-full w-[0.9em] h-[0.9em];
|
@apply rounded-full w-[0.9em] h-[0.9em];
|
||||||
|
|
||||||
content: '';
|
content: '';
|
||||||
|
|
||||||
&.cm-lint-marker-error {
|
&.cm-lint-marker-error {
|
||||||
|
|||||||
@@ -290,6 +290,8 @@ export function Editor({
|
|||||||
showDialog({
|
showDialog({
|
||||||
id: 'template-function-' + Math.random(), // Allow multiple at once
|
id: 'template-function-' + Math.random(), // Allow multiple at once
|
||||||
size: 'md',
|
size: 'md',
|
||||||
|
className: 'h-[90vh]',
|
||||||
|
noPadding: true,
|
||||||
title: <InlineCode>{fn.name}(…)</InlineCode>,
|
title: <InlineCode>{fn.name}(…)</InlineCode>,
|
||||||
description: fn.description,
|
description: fn.description,
|
||||||
render: ({ hide }) => {
|
render: ({ hide }) => {
|
||||||
@@ -354,6 +356,7 @@ export function Editor({
|
|||||||
const ext = getLanguageExtension({
|
const ext = getLanguageExtension({
|
||||||
useTemplating,
|
useTemplating,
|
||||||
language,
|
language,
|
||||||
|
hideGutter,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
autocomplete,
|
autocomplete,
|
||||||
completionOptions,
|
completionOptions,
|
||||||
@@ -374,6 +377,7 @@ export function Editor({
|
|||||||
completionOptions,
|
completionOptions,
|
||||||
useTemplating,
|
useTemplating,
|
||||||
graphQLSchema,
|
graphQLSchema,
|
||||||
|
hideGutter,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Initialize the editor when ref mounts
|
// Initialize the editor when ref mounts
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export function getLanguageExtension({
|
|||||||
language = 'text',
|
language = 'text',
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
autocomplete,
|
autocomplete,
|
||||||
|
hideGutter,
|
||||||
onClickVariable,
|
onClickVariable,
|
||||||
onClickMissingVariable,
|
onClickMissingVariable,
|
||||||
onClickPathParameter,
|
onClickPathParameter,
|
||||||
@@ -118,7 +119,7 @@ export function getLanguageExtension({
|
|||||||
onClickPathParameter: (name: string) => void;
|
onClickPathParameter: (name: string) => void;
|
||||||
completionOptions: TwigCompletionOption[];
|
completionOptions: TwigCompletionOption[];
|
||||||
graphQLSchema: GraphQLSchema | null;
|
graphQLSchema: GraphQLSchema | null;
|
||||||
} & Pick<EditorProps, 'language' | 'autocomplete'>) {
|
} & Pick<EditorProps, 'language' | 'autocomplete' | 'hideGutter'>) {
|
||||||
const extraExtensions: Extension[] = [];
|
const extraExtensions: Extension[] = [];
|
||||||
|
|
||||||
if (language === 'url') {
|
if (language === 'url') {
|
||||||
@@ -155,7 +156,10 @@ export function getLanguageExtension({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (language === 'json') {
|
if (language === 'json') {
|
||||||
extraExtensions.push(linter(jsonParseLinter()), lintGutter());
|
extraExtensions.push(linter(jsonParseLinter()));
|
||||||
|
if (!hideGutter) {
|
||||||
|
extraExtensions.push(lintGutter());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maybeBase = language ? syntaxExtensions[language] : null;
|
const maybeBase = language ? syntaxExtensions[language] : null;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function useHttpAuthenticationConfig(
|
|||||||
],
|
],
|
||||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (authName == null) return null;
|
if (authName == null || authName === 'inherit') return null;
|
||||||
const config = await invokeCmd<GetHttpAuthenticationConfigResponse>(
|
const config = await invokeCmd<GetHttpAuthenticationConfigResponse>(
|
||||||
'cmd_get_http_authentication_config',
|
'cmd_get_http_authentication_config',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,16 +45,25 @@ export function useTemplateFunctionConfig(
|
|||||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (functionName == null) return null;
|
if (functionName == null) return null;
|
||||||
const config = await invokeCmd<GetTemplateFunctionConfigResponse>(
|
return getTemplateFunctionConfig(functionName, values, model, environmentId);
|
||||||
'cmd_template_function_config',
|
|
||||||
{
|
|
||||||
functionName: functionName,
|
|
||||||
values,
|
|
||||||
model,
|
|
||||||
environmentId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return config.function;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTemplateFunctionConfig(
|
||||||
|
functionName: string,
|
||||||
|
values: Record<string, JsonPrimitive>,
|
||||||
|
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
|
||||||
|
environmentId: string | undefined,
|
||||||
|
) {
|
||||||
|
const config = await invokeCmd<GetTemplateFunctionConfigResponse>(
|
||||||
|
'cmd_template_function_config',
|
||||||
|
{
|
||||||
|
functionName,
|
||||||
|
values,
|
||||||
|
model,
|
||||||
|
environmentId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return config.function;
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export async function editEnvironment(
|
|||||||
id: 'environment-editor',
|
id: 'environment-editor',
|
||||||
noPadding: true,
|
noPadding: true,
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
className: 'h-[80vh]',
|
className: 'h-[90vh]',
|
||||||
render: () => (
|
render: () => (
|
||||||
<EnvironmentEditDialog
|
<EnvironmentEditDialog
|
||||||
initialEnvironmentId={environment?.id ?? null}
|
initialEnvironmentId={environment?.id ?? null}
|
||||||
|
|||||||
Reference in New Issue
Block a user