mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-21 00:49:45 +01: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",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.9.8",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"xpath": "^0.0.34"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonpath": "^0.2.4"
|
||||
"@yaak/template-function-xml": "*"
|
||||
}
|
||||
},
|
||||
"plugins/template-function-timestamp": {
|
||||
|
||||
@@ -30,7 +30,7 @@ export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
|
||||
|
||||
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, };
|
||||
|
||||
@@ -74,7 +74,7 @@ export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
|
||||
|
||||
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, };
|
||||
|
||||
@@ -224,6 +224,8 @@ defaultValue?: string, disabled?: boolean,
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputHStack = { inputs?: Array<FormInput>, };
|
||||
|
||||
export type FormInputHttpRequest = {
|
||||
/**
|
||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||
|
||||
@@ -3,22 +3,37 @@ import {
|
||||
CallHttpAuthenticationRequest,
|
||||
CallHttpAuthenticationResponse,
|
||||
FormInput,
|
||||
GetHttpAuthenticationConfigRequest,
|
||||
GetHttpAuthenticationSummaryResponse,
|
||||
HttpAuthenticationAction,
|
||||
} from '../bindings/gen_events';
|
||||
import { MaybePromise } from '../helpers';
|
||||
import { Context } from './Context';
|
||||
|
||||
type DynamicFormInput = FormInput & {
|
||||
dynamic(
|
||||
type AddDynamicMethod<T> = {
|
||||
dynamic?: (
|
||||
ctx: Context,
|
||||
args: GetHttpAuthenticationConfigRequest,
|
||||
): MaybePromise<Partial<FormInput> | undefined | null>;
|
||||
args: CallHttpAuthenticationActionArgs,
|
||||
) => 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 & {
|
||||
args: (FormInput | DynamicFormInput)[];
|
||||
args: DynamicAuthenticationArg[];
|
||||
onApply(
|
||||
ctx: Context,
|
||||
args: CallHttpAuthenticationRequest,
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import {
|
||||
CallTemplateFunctionArgs,
|
||||
FormInput,
|
||||
GetHttpAuthenticationConfigRequest,
|
||||
TemplateFunction,
|
||||
TemplateFunctionArg,
|
||||
} from '../bindings/gen_events';
|
||||
import { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events';
|
||||
import { MaybePromise } from '../helpers';
|
||||
import { Context } from './Context';
|
||||
|
||||
export type DynamicTemplateFunctionArg = FormInput & {
|
||||
dynamic(
|
||||
type AddDynamicMethod<T> = {
|
||||
dynamic?: (
|
||||
ctx: Context,
|
||||
args: GetHttpAuthenticationConfigRequest,
|
||||
): MaybePromise<Partial<FormInput> | undefined | null>;
|
||||
args: CallTemplateFunctionArgs,
|
||||
) => MaybePromise<Partial<T> | null | undefined>;
|
||||
};
|
||||
|
||||
export type TemplateFunctionPlugin = TemplateFunction & {
|
||||
args: (TemplateFunctionArg | DynamicTemplateFunctionArg)[];
|
||||
type AddDynamic<T> = T extends any
|
||||
? 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>;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { AuthenticationPlugin } from './AuthenticationPlugin';
|
||||
|
||||
import type { Context } from './Context';
|
||||
import type { FilterPlugin } from './FilterPlugin';
|
||||
import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
|
||||
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
|
||||
@@ -6,9 +8,10 @@ import type { ImporterPlugin } from './ImporterPlugin';
|
||||
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
|
||||
import type { ThemePlugin } from './ThemePlugin';
|
||||
|
||||
import type { Context } from './Context';
|
||||
|
||||
export type { Context };
|
||||
export type { DynamicTemplateFunctionArg } from './TemplateFunctionPlugin';
|
||||
export type { DynamicAuthenticationArg } from './AuthenticationPlugin';
|
||||
export type { TemplateFunctionPlugin };
|
||||
|
||||
/**
|
||||
* The global structure of a Yaak plugin
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
BootRequest,
|
||||
DeleteKeyValueResponse,
|
||||
FindHttpResponsesResponse,
|
||||
FormInput,
|
||||
GetCookieValueRequest,
|
||||
GetCookieValueResponse,
|
||||
GetHttpRequestByIdResponse,
|
||||
@@ -19,14 +18,13 @@ import {
|
||||
RenderHttpRequestResponse,
|
||||
SendHttpRequestResponse,
|
||||
TemplateFunction,
|
||||
TemplateFunctionArg,
|
||||
TemplateRenderResponse,
|
||||
} from '@yaakapp-internal/plugins';
|
||||
import { Context, PluginDefinition } from '@yaakapp/api';
|
||||
import { JsonValue } from '@yaakapp/api/lib/bindings/serde_json/JsonValue';
|
||||
import console from 'node:console';
|
||||
import { type Stats, statSync, watch } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { applyDynamicFormInput, applyFormInputDefaults } from './common';
|
||||
import { EventChannel } from './EventChannel';
|
||||
import { migrateTemplateFunctionSelectOptions } from './migrations';
|
||||
|
||||
@@ -213,24 +211,19 @@ export class PluginInstance {
|
||||
return;
|
||||
}
|
||||
|
||||
templateFunction = migrateTemplateFunctionSelectOptions(templateFunction);
|
||||
// @ts-ignore
|
||||
delete templateFunction.onRender;
|
||||
const resolvedArgs: TemplateFunctionArg[] = [];
|
||||
for (const arg of templateFunction.args) {
|
||||
if (arg && 'dynamic' in arg) {
|
||||
const dynamicAttrs = await arg.dynamic(ctx, payload);
|
||||
const { dynamic, ...other } = arg;
|
||||
resolvedArgs.push({ ...other, ...dynamicAttrs } as TemplateFunctionArg);
|
||||
} else if (arg) {
|
||||
resolvedArgs.push(arg);
|
||||
}
|
||||
templateFunction.args = resolvedArgs;
|
||||
}
|
||||
const fn = {
|
||||
...migrateTemplateFunctionSelectOptions(templateFunction),
|
||||
onRender: undefined,
|
||||
};
|
||||
|
||||
payload.values = applyFormInputDefaults(fn.args, payload.values);
|
||||
const p = { ...payload, purpose: 'preview' } as const;
|
||||
const resolvedArgs = await applyDynamicFormInput(ctx, fn.args, p);
|
||||
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_template_function_config_response',
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
function: templateFunction,
|
||||
function: { ...fn, args: resolvedArgs },
|
||||
};
|
||||
this.#sendPayload(context, replyPayload, replyId);
|
||||
return;
|
||||
@@ -248,16 +241,8 @@ export class PluginInstance {
|
||||
|
||||
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
|
||||
const { args, actions } = this.#mod.authentication;
|
||||
const resolvedArgs: FormInput[] = [];
|
||||
for (const v of args) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
payload.values = applyFormInputDefaults(args, payload.values);
|
||||
const resolvedArgs = await applyDynamicFormInput(ctx, args, payload);
|
||||
const resolvedActions: HttpAuthenticationAction[] = [];
|
||||
for (const { onSelect, ...action } of actions ?? []) {
|
||||
resolvedActions.push(action);
|
||||
@@ -277,7 +262,8 @@ export class PluginInstance {
|
||||
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) {
|
||||
const auth = this.#mod.authentication;
|
||||
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(
|
||||
context,
|
||||
{
|
||||
@@ -332,7 +318,8 @@ export class PluginInstance {
|
||||
) {
|
||||
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
|
||||
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 {
|
||||
const result = await fn.onRender(ctx, payload.args);
|
||||
this.#sendPayload(
|
||||
@@ -652,20 +639,6 @@ function genId(len = 5): string {
|
||||
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> = {};
|
||||
|
||||
/**
|
||||
|
||||
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(
|
||||
f: TemplateFunctionPlugin,
|
||||
@@ -13,8 +13,5 @@ export function migrateTemplateFunctionSelectOptions(
|
||||
return a;
|
||||
});
|
||||
|
||||
return {
|
||||
...f,
|
||||
args: migratedArgs,
|
||||
};
|
||||
return { ...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(
|
||||
{
|
||||
host: url.host,
|
||||
@@ -68,6 +64,7 @@ export const plugin: PluginDefinition = {
|
||||
path: url.pathname + (url.search || ''),
|
||||
service: String(values.service || 'sts'),
|
||||
region: values.region ? String(values.region) : undefined,
|
||||
body: values.body ? String(values.body) : undefined,
|
||||
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) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -288,6 +288,7 @@ export const plugin: PluginDefinition = {
|
||||
{
|
||||
type: 'accordion',
|
||||
label: 'Access Token Response',
|
||||
inputs: [],
|
||||
async dynamic(ctx, { contextId, values }) {
|
||||
const tokenArgs: TokenStoreArgs = {
|
||||
contextId,
|
||||
@@ -304,6 +305,7 @@ export const plugin: PluginDefinition = {
|
||||
inputs: [
|
||||
{
|
||||
type: 'editor',
|
||||
name: 'response',
|
||||
defaultValue: JSON.stringify(token.response, null, 2),
|
||||
hideLabel: true,
|
||||
readOnly: true,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const UTF8 = 'utf8';
|
||||
const options = [
|
||||
{ label: 'ASCII', value: 'ascii' },
|
||||
{ label: 'UTF-8', value: 'utf8' },
|
||||
{ label: 'UTF-8', value: UTF8 },
|
||||
{ label: 'UTF-16 LE', value: 'utf16le' },
|
||||
{ label: 'Base64', value: 'base64' },
|
||||
{ label: 'Base64 URL-safe', value: 'base64url' },
|
||||
@@ -18,12 +19,11 @@ export const plugin: PluginDefinition = {
|
||||
args: [
|
||||
{ title: 'Select File', type: 'file', name: 'path', label: 'File' },
|
||||
{
|
||||
title: 'Select encoding',
|
||||
type: 'select',
|
||||
name: 'encoding',
|
||||
label: 'Encoding',
|
||||
defaultValue: 'utf8',
|
||||
description: 'Specifies how the file’s bytes are decoded into text when read',
|
||||
defaultValue: UTF8,
|
||||
description: "Specifies how the file's bytes are decoded into text when read",
|
||||
options,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"description": "Template functions for working with JSON data",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { XPathResult } from '@yaak/template-function-xml';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
|
||||
const RETURN_FIRST = 'first';
|
||||
const RETURN_ALL = 'all';
|
||||
const RETURN_JOIN = 'join';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
@@ -8,32 +13,59 @@ export const plugin: PluginDefinition = {
|
||||
description: 'Filter JSON-formatted text using JSONPath syntax',
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
type: 'editor',
|
||||
name: 'input',
|
||||
label: 'Input',
|
||||
multiLine: true,
|
||||
language: 'json',
|
||||
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: 'checkbox', name: 'formatted', label: 'Format Output' },
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
try {
|
||||
const parsed = JSON.parse(String(args.values.input));
|
||||
const query = String(args.values.query ?? '$').trim();
|
||||
let filtered = JSONPath({ path: query, json: parsed });
|
||||
if (Array.isArray(filtered)) {
|
||||
filtered = filtered[0];
|
||||
}
|
||||
if (typeof filtered === 'string') {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
if (args.values.formatted) {
|
||||
return JSON.stringify(filtered, null, 2);
|
||||
} else {
|
||||
return JSON.stringify(filtered);
|
||||
}
|
||||
console.log('formatted', args.values.formatted);
|
||||
return filterJSONPath(
|
||||
String(args.values.input),
|
||||
String(args.values.query),
|
||||
(args.values.result || RETURN_FIRST) as XPathResult,
|
||||
args.values.join == null ? null : String(args.values.join),
|
||||
Boolean(args.values.formatted),
|
||||
);
|
||||
} catch {
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"xpath": "^0.0.34",
|
||||
"@xmldom/xmldom": "^0.9.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonpath": "^0.2.4"
|
||||
"@yaak/template-function-xml": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
CallTemplateFunctionArgs,
|
||||
Context,
|
||||
DynamicTemplateFunctionArg,
|
||||
FormInput,
|
||||
GetHttpAuthenticationConfigRequest,
|
||||
HttpResponse,
|
||||
PluginDefinition,
|
||||
RenderPurpose,
|
||||
} from '@yaakapp/api';
|
||||
import type { DynamicTemplateFunctionArg } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import xpath from 'xpath';
|
||||
|
||||
const BEHAVIOR_TTL = 'ttl';
|
||||
const BEHAVIOR_ALWAYS = 'always';
|
||||
const BEHAVIOR_SMART = 'smart';
|
||||
|
||||
const behaviorArg: FormInput = {
|
||||
type: 'select',
|
||||
name: 'behavior',
|
||||
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 RETURN_FIRST = 'first';
|
||||
const RETURN_ALL = 'all';
|
||||
const RETURN_JOIN = 'join';
|
||||
|
||||
const ttlArg: DynamicTemplateFunctionArg = {
|
||||
type: 'text',
|
||||
name: 'ttl',
|
||||
label: 'Expiration Time (seconds)',
|
||||
placeholder: '0',
|
||||
description: 'Resend the request when the latest response is older than this many seconds, or if there are no responses yet.',
|
||||
dynamic(_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) {
|
||||
const show = values.behavior === BEHAVIOR_TTL;
|
||||
return { hidden: !show };
|
||||
},
|
||||
const behaviorArgs: DynamicTemplateFunctionArg = {
|
||||
type: 'h_stack',
|
||||
inputs: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'behavior',
|
||||
label: 'Sending Behavior',
|
||||
defaultValue: BEHAVIOR_SMART,
|
||||
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 = {
|
||||
@@ -54,14 +63,13 @@ export const plugin: PluginDefinition = {
|
||||
description: 'Read the value of a response header, by name',
|
||||
args: [
|
||||
requestArg,
|
||||
behaviorArgs,
|
||||
{
|
||||
type: 'text',
|
||||
name: 'header',
|
||||
label: 'Header Name',
|
||||
placeholder: 'Content-Type',
|
||||
},
|
||||
behaviorArg,
|
||||
ttlArg,
|
||||
],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
if (!args.values.request || !args.values.header) return null;
|
||||
@@ -86,14 +94,67 @@ export const plugin: PluginDefinition = {
|
||||
aliases: ['response'],
|
||||
args: [
|
||||
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',
|
||||
name: 'path',
|
||||
label: 'JSONPath or XPath',
|
||||
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> {
|
||||
if (!args.values.request || !args.values.path) return null;
|
||||
@@ -118,13 +179,35 @@ export const plugin: PluginDefinition = {
|
||||
}
|
||||
|
||||
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 {
|
||||
// Probably not JSON, try XPath
|
||||
}
|
||||
|
||||
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 {
|
||||
// Probably not XML
|
||||
}
|
||||
@@ -136,7 +219,7 @@ export const plugin: PluginDefinition = {
|
||||
name: 'response.body.raw',
|
||||
description: 'Access the entire response body, as text',
|
||||
aliases: ['response'],
|
||||
args: [requestArg, behaviorArg, ttlArg],
|
||||
args: [requestArg, behaviorArgs],
|
||||
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | 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(
|
||||
ctx: Context,
|
||||
{
|
||||
@@ -244,8 +297,8 @@ async function getResponse(
|
||||
|
||||
function shouldSendExpired(response: HttpResponse | null, ttl: string | null): boolean {
|
||||
if (response == null) return true;
|
||||
const ttlSeconds = parseInt(ttl || '0');
|
||||
if (isNaN(ttlSeconds)) throw new Error(`Invalid TTL "${ttl}"`);
|
||||
const ttlSeconds = parseInt(ttl || '0') || 0;
|
||||
if (ttlSeconds === 0) return false;
|
||||
const nowMillis = Date.now();
|
||||
const respMillis = new Date(response.createdAt + 'Z').getTime();
|
||||
return respMillis + ttlSeconds * 1000 < nowMillis;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"description": "Template functions for working with XML data",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
|
||||
@@ -2,6 +2,10 @@ import { DOMParser } from '@xmldom/xmldom';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import xpath from 'xpath';
|
||||
|
||||
const RETURN_FIRST = 'first';
|
||||
const RETURN_ALL = 'all';
|
||||
const RETURN_JOIN = 'join';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
@@ -15,20 +19,39 @@ export const plugin: PluginDefinition = {
|
||||
multiLine: true,
|
||||
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' },
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const doc: any = new DOMParser().parseFromString(String(args.values.input), 'text/xml');
|
||||
const result = xpath.select(String(args.values.query), doc, false);
|
||||
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);
|
||||
}
|
||||
const result = (args.values.result || RETURN_FIRST) as XPathResult;
|
||||
const join = args.values.join == null ? null : String(args.values.join);
|
||||
return filterXPath(String(args.values.input), String(args.values.query), result, join);
|
||||
} catch {
|
||||
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,
|
||||
plugin_handle: &PluginHandle,
|
||||
) -> Result<Option<InternalEventPayload>> {
|
||||
// debug!("Got event to app {event:?}");
|
||||
// log::debug!("Got event to app {event:?}");
|
||||
let plugin_context = event.context.to_owned();
|
||||
match event.clone().payload {
|
||||
InternalEventPayload::CopyTextRequest(req) => {
|
||||
|
||||
@@ -3,11 +3,11 @@ use crate::error::Error::ModelNotFound;
|
||||
use crate::error::Result;
|
||||
use crate::models::{AnyModel, UpsertModelInfo};
|
||||
use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource};
|
||||
use log::{error, warn};
|
||||
use log::error;
|
||||
use rusqlite::OptionalExtension;
|
||||
use sea_query::{
|
||||
Alias, Asterisk, Expr, Func, IntoColumnRef, IntoIden, IntoTableRef, OnConflict, Query,
|
||||
ReturningClause, SimpleExpr, SqliteQueryBuilder,
|
||||
Asterisk, Expr, Func, IntoColumnRef, IntoIden, IntoTableRef, OnConflict, Query, SimpleExpr,
|
||||
SqliteQueryBuilder,
|
||||
};
|
||||
use sea_query_rusqlite::RusqliteBinder;
|
||||
use std::fmt::Debug;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::commands::*;
|
||||
use crate::migrate::migrate_db;
|
||||
use crate::query_manager::QueryManager;
|
||||
use crate::util::ModelChangeEvent;
|
||||
use log::error;
|
||||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
|
||||
@@ -30,7 +30,7 @@ export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
|
||||
|
||||
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, };
|
||||
|
||||
@@ -74,7 +74,7 @@ export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
|
||||
|
||||
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, };
|
||||
|
||||
@@ -224,6 +224,8 @@ defaultValue?: string, disabled?: boolean,
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputHStack = { inputs?: Array<FormInput>, };
|
||||
|
||||
export type FormInputHttpRequest = {
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
|
||||
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)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_events.ts")]
|
||||
@@ -733,6 +745,7 @@ pub enum FormInput {
|
||||
File(FormInputFile),
|
||||
HttpRequest(FormInputHttpRequest),
|
||||
Accordion(FormInputAccordion),
|
||||
HStack(FormInputHStack),
|
||||
Banner(FormInputBanner),
|
||||
Markdown(FormInputMarkdown),
|
||||
}
|
||||
@@ -895,7 +908,7 @@ pub struct FormInputFile {
|
||||
#[ts(optional)]
|
||||
pub directory: Option<bool>,
|
||||
|
||||
// Default file path for selection dialog
|
||||
// Default file path for the selection dialog
|
||||
#[ts(optional)]
|
||||
pub default_path: Option<String>,
|
||||
|
||||
@@ -953,6 +966,14 @@ pub struct FormInputAccordion {
|
||||
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)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_events.ts")]
|
||||
@@ -1015,7 +1036,7 @@ pub struct CallTemplateFunctionResponse {
|
||||
#[ts(export, export_to = "gen_events.ts")]
|
||||
pub struct CallTemplateFunctionArgs {
|
||||
pub purpose: RenderPurpose,
|
||||
pub values: HashMap<String, serde_json::Value>,
|
||||
pub values: HashMap<String, JsonPrimitive>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
|
||||
@@ -786,7 +786,7 @@ impl PluginManager {
|
||||
&self,
|
||||
plugin_context: &PluginContext,
|
||||
fn_name: &str,
|
||||
values: HashMap<String, serde_json::Value>,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
purpose: RenderPurpose,
|
||||
) -> TemplateResult<String> {
|
||||
let req = CallTemplateFunctionRequest {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::events::{PluginContext, RenderPurpose};
|
||||
use crate::events::{JsonPrimitive, PluginContext, RenderPurpose};
|
||||
use crate::manager::PluginManager;
|
||||
use crate::native_template_functions::{
|
||||
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);
|
||||
}
|
||||
|
||||
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 resp = plugin_manager
|
||||
.call_template_function(
|
||||
&self.plugin_context,
|
||||
fn_name,
|
||||
args,
|
||||
primitive_args,
|
||||
self.render_purpose.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -26,7 +26,7 @@ import { IconButton } from './core/IconButton';
|
||||
import { Input } from './core/Input';
|
||||
import { Label } from './core/Label';
|
||||
import { Select } from './core/Select';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { Markdown } from './Markdown';
|
||||
import { SelectFile } from './SelectFile';
|
||||
|
||||
@@ -41,6 +41,7 @@ interface Props<T> {
|
||||
autocompleteFunctions?: boolean;
|
||||
autocompleteVariables?: boolean;
|
||||
stateKey: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -51,6 +52,7 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
||||
autocompleteVariables,
|
||||
autocompleteFunctions,
|
||||
stateKey,
|
||||
className,
|
||||
disabled,
|
||||
}: Props<T>) {
|
||||
const setDataAttr = useCallback(
|
||||
@@ -61,7 +63,7 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
||||
);
|
||||
|
||||
return (
|
||||
<FormInputs
|
||||
<FormInputsStack
|
||||
disabled={disabled}
|
||||
inputs={inputs}
|
||||
setDataAttr={setDataAttr}
|
||||
@@ -69,28 +71,15 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
||||
autocompleteFunctions={autocompleteFunctions}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
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>>({
|
||||
inputs,
|
||||
autocompleteFunctions,
|
||||
autocompleteVariables,
|
||||
stateKey,
|
||||
setDataAttr,
|
||||
data,
|
||||
disabled,
|
||||
function FormInputsStack<T extends Record<string, JsonPrimitive>>({
|
||||
className,
|
||||
}: Pick<
|
||||
Props<T>,
|
||||
'inputs' | 'autocompleteFunctions' | 'autocompleteVariables' | 'stateKey' | 'data'
|
||||
> & {
|
||||
setDataAttr: (name: string, value: JsonPrimitive) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
...props
|
||||
}: FormInputsProps<T> & { className?: string }) {
|
||||
return (
|
||||
<VStack
|
||||
space={3}
|
||||
@@ -100,6 +89,30 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
'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) => {
|
||||
if ('hidden' in input && input.hidden) {
|
||||
return null;
|
||||
@@ -113,7 +126,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
case 'select':
|
||||
return (
|
||||
<SelectArg
|
||||
key={i + stateKey}
|
||||
key={i}
|
||||
arg={input}
|
||||
onChange={(v) => setDataAttr(input.name, v)}
|
||||
value={
|
||||
@@ -126,7 +139,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
case 'text':
|
||||
return (
|
||||
<TextArg
|
||||
key={i}
|
||||
key={i + stateKey}
|
||||
stateKey={stateKey}
|
||||
arg={input}
|
||||
autocompleteFunctions={autocompleteFunctions || false}
|
||||
@@ -140,7 +153,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
case 'editor':
|
||||
return (
|
||||
<EditorArg
|
||||
key={i}
|
||||
key={i + stateKey}
|
||||
stateKey={stateKey}
|
||||
arg={input}
|
||||
autocompleteFunctions={autocompleteFunctions || false}
|
||||
@@ -182,13 +195,13 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
);
|
||||
case 'accordion':
|
||||
return (
|
||||
<div key={i}>
|
||||
<div key={i + stateKey}>
|
||||
<DetailsBanner
|
||||
summary={input.label}
|
||||
className={classNames('!mb-auto', disabled && 'opacity-disabled')}
|
||||
>
|
||||
<div className="my-3">
|
||||
<FormInputs
|
||||
<FormInputsStack
|
||||
data={data}
|
||||
disabled={disabled}
|
||||
inputs={input.inputs}
|
||||
@@ -201,14 +214,28 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
</DetailsBanner>
|
||||
</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':
|
||||
return (
|
||||
<Banner
|
||||
key={i}
|
||||
key={i + stateKey}
|
||||
color={input.color}
|
||||
className={classNames(disabled && 'opacity-disabled')}
|
||||
>
|
||||
<FormInputs
|
||||
<FormInputsStack
|
||||
data={data}
|
||||
disabled={disabled}
|
||||
inputs={input.inputs}
|
||||
@@ -220,10 +247,10 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
</Banner>
|
||||
);
|
||||
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'}
|
||||
label={arg.label ?? arg.name}
|
||||
size={INPUT_SIZE}
|
||||
hideLabel={arg.label == null}
|
||||
hideLabel={arg.hideLabel ?? arg.label == null}
|
||||
placeholder={arg.placeholder ?? undefined}
|
||||
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
|
||||
autocompleteFunctions={autocompleteFunctions}
|
||||
@@ -313,7 +340,9 @@ function EditorArg({
|
||||
language={arg.language}
|
||||
readOnly={arg.readOnly}
|
||||
onChange={onChange}
|
||||
hideGutter
|
||||
heightMode="auto"
|
||||
className="min-h-[3rem]"
|
||||
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
|
||||
placeholder={arg.placeholder ?? undefined}
|
||||
autocompleteFunctions={autocompleteFunctions}
|
||||
@@ -374,7 +403,6 @@ function EditorArg({
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
hideGutter
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -396,6 +424,7 @@ function SelectArg({
|
||||
name={arg.name}
|
||||
help={arg.description}
|
||||
onChange={onChange}
|
||||
defaultValue={arg.defaultValue}
|
||||
hideLabel={arg.hideLabel}
|
||||
value={value}
|
||||
size={INPUT_SIZE}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
WebsocketRequest,
|
||||
Workspace,
|
||||
} 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 classNames from 'classnames';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@@ -45,24 +45,7 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
|
||||
}
|
||||
|
||||
(async function () {
|
||||
const initial: Record<string, string> = {};
|
||||
const initialArgs =
|
||||
initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'fn'
|
||||
? initialTokens.tokens[0]?.val.args
|
||||
: [];
|
||||
for (const arg of templateFunction.args) {
|
||||
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;
|
||||
}
|
||||
const initial = collectArgumentValues(initialTokens, templateFunction);
|
||||
|
||||
// HACK: Replace the secure() function's encrypted `value` arg with the decrypted version so
|
||||
// we can display it in the editor input.
|
||||
@@ -71,12 +54,14 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
|
||||
initial.value = await convertTemplateToInsecure(template);
|
||||
}
|
||||
|
||||
console.log('INITIAL', initial);
|
||||
setInitialArgValues(initial);
|
||||
})().catch(console.error);
|
||||
}, [
|
||||
initialArgValues,
|
||||
initialTokens,
|
||||
initialTokens.tokens,
|
||||
templateFunction,
|
||||
templateFunction.args,
|
||||
templateFunction.name,
|
||||
]);
|
||||
@@ -159,84 +144,117 @@ function InitializedTemplateFunctionDialog({
|
||||
if (templateFunction == null) return null;
|
||||
|
||||
return (
|
||||
<VStack
|
||||
as="form"
|
||||
className="pb-3"
|
||||
space={4}
|
||||
<form
|
||||
className="grid grid-rows-[minmax(0,1fr)_auto_auto] h-full max-h-[90vh]"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleDone();
|
||||
}}
|
||||
>
|
||||
{name === 'secure' ? (
|
||||
<PlainInput
|
||||
required
|
||||
label="Value"
|
||||
name="value"
|
||||
type="password"
|
||||
placeholder="••••••••••••"
|
||||
defaultValue={String(argValues['value'] ?? '')}
|
||||
onChange={(value) => setArgValues({ ...argValues, value })}
|
||||
/>
|
||||
) : (
|
||||
<DynamicForm
|
||||
autocompleteVariables
|
||||
autocompleteFunctions
|
||||
inputs={templateFunction.args}
|
||||
data={argValues}
|
||||
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>
|
||||
<div className="overflow-y-auto h-full px-6">
|
||||
{name === 'secure' ? (
|
||||
<PlainInput
|
||||
required
|
||||
label="Value"
|
||||
name="value"
|
||||
type="password"
|
||||
placeholder="••••••••••••"
|
||||
defaultValue={String(argValues['value'] ?? '')}
|
||||
onChange={(value) => setArgValues({ ...argValues, value })}
|
||||
/>
|
||||
) : (
|
||||
<DynamicForm
|
||||
autocompleteVariables
|
||||
autocompleteFunctions
|
||||
inputs={templateFunction.args}
|
||||
data={argValues}
|
||||
onChange={setArgValues}
|
||||
stateKey={`template_function.${templateFunction.name}`}
|
||||
/>
|
||||
)}
|
||||
<Button type="submit" color="primary">
|
||||
Save
|
||||
</Button>
|
||||
</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,
|
||||
'h-xs', // More compact
|
||||
'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 === 'primary' && '!text-primary',
|
||||
item.color === 'success' && '!text-success',
|
||||
|
||||
@@ -78,9 +78,19 @@
|
||||
@apply cursor-default;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.cm-gutter-lint {
|
||||
@apply w-auto !important;
|
||||
|
||||
.cm-gutterElement {
|
||||
@apply px-0;
|
||||
}
|
||||
|
||||
.cm-lint-marker {
|
||||
@apply cursor-default opacity-80 hover:opacity-100 transition-opacity;
|
||||
@apply rounded-full w-[0.9em] h-[0.9em];
|
||||
|
||||
content: '';
|
||||
|
||||
&.cm-lint-marker-error {
|
||||
|
||||
@@ -290,6 +290,8 @@ export function Editor({
|
||||
showDialog({
|
||||
id: 'template-function-' + Math.random(), // Allow multiple at once
|
||||
size: 'md',
|
||||
className: 'h-[90vh]',
|
||||
noPadding: true,
|
||||
title: <InlineCode>{fn.name}(…)</InlineCode>,
|
||||
description: fn.description,
|
||||
render: ({ hide }) => {
|
||||
@@ -354,6 +356,7 @@ export function Editor({
|
||||
const ext = getLanguageExtension({
|
||||
useTemplating,
|
||||
language,
|
||||
hideGutter,
|
||||
environmentVariables,
|
||||
autocomplete,
|
||||
completionOptions,
|
||||
@@ -374,6 +377,7 @@ export function Editor({
|
||||
completionOptions,
|
||||
useTemplating,
|
||||
graphQLSchema,
|
||||
hideGutter,
|
||||
]);
|
||||
|
||||
// Initialize the editor when ref mounts
|
||||
|
||||
@@ -105,6 +105,7 @@ export function getLanguageExtension({
|
||||
language = 'text',
|
||||
environmentVariables,
|
||||
autocomplete,
|
||||
hideGutter,
|
||||
onClickVariable,
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
@@ -118,7 +119,7 @@ export function getLanguageExtension({
|
||||
onClickPathParameter: (name: string) => void;
|
||||
completionOptions: TwigCompletionOption[];
|
||||
graphQLSchema: GraphQLSchema | null;
|
||||
} & Pick<EditorProps, 'language' | 'autocomplete'>) {
|
||||
} & Pick<EditorProps, 'language' | 'autocomplete' | 'hideGutter'>) {
|
||||
const extraExtensions: Extension[] = [];
|
||||
|
||||
if (language === 'url') {
|
||||
@@ -155,7 +156,10 @@ export function getLanguageExtension({
|
||||
}
|
||||
|
||||
if (language === 'json') {
|
||||
extraExtensions.push(linter(jsonParseLinter()), lintGutter());
|
||||
extraExtensions.push(linter(jsonParseLinter()));
|
||||
if (!hideGutter) {
|
||||
extraExtensions.push(lintGutter());
|
||||
}
|
||||
}
|
||||
|
||||
const maybeBase = language ? syntaxExtensions[language] : null;
|
||||
|
||||
@@ -47,7 +47,7 @@ export function useHttpAuthenticationConfig(
|
||||
],
|
||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||
queryFn: async () => {
|
||||
if (authName == null) return null;
|
||||
if (authName == null || authName === 'inherit') return null;
|
||||
const config = await invokeCmd<GetHttpAuthenticationConfigResponse>(
|
||||
'cmd_get_http_authentication_config',
|
||||
{
|
||||
|
||||
@@ -45,16 +45,25 @@ export function useTemplateFunctionConfig(
|
||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||
queryFn: async () => {
|
||||
if (functionName == null) return null;
|
||||
const config = await invokeCmd<GetTemplateFunctionConfigResponse>(
|
||||
'cmd_template_function_config',
|
||||
{
|
||||
functionName: functionName,
|
||||
values,
|
||||
model,
|
||||
environmentId,
|
||||
},
|
||||
);
|
||||
return config.function;
|
||||
return getTemplateFunctionConfig(functionName, values, model, environmentId);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
noPadding: true,
|
||||
size: 'lg',
|
||||
className: 'h-[80vh]',
|
||||
className: 'h-[90vh]',
|
||||
render: () => (
|
||||
<EnvironmentEditDialog
|
||||
initialEnvironmentId={environment?.id ?? null}
|
||||
|
||||
Reference in New Issue
Block a user