mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 09:48:28 +02:00
add template-function-datetime (#244)
This commit is contained in:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -26,6 +26,7 @@
|
|||||||
"plugins/importer-postman",
|
"plugins/importer-postman",
|
||||||
"plugins/importer-yaak",
|
"plugins/importer-yaak",
|
||||||
"plugins/template-function-cookie",
|
"plugins/template-function-cookie",
|
||||||
|
"plugins/template-function-datetime",
|
||||||
"plugins/template-function-encode",
|
"plugins/template-function-encode",
|
||||||
"plugins/template-function-fs",
|
"plugins/template-function-fs",
|
||||||
"plugins/template-function-hash",
|
"plugins/template-function-hash",
|
||||||
@@ -4142,6 +4143,10 @@
|
|||||||
"resolved": "plugins/template-function-cookie",
|
"resolved": "plugins/template-function-cookie",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@yaak/template-function-datetime": {
|
||||||
|
"resolved": "plugins/template-function-datetime",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@yaak/template-function-encode": {
|
"node_modules/@yaak/template-function-encode": {
|
||||||
"resolved": "plugins/template-function-encode",
|
"resolved": "plugins/template-function-encode",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -18585,6 +18590,12 @@
|
|||||||
"name": "@yaak/template-function-cookie",
|
"name": "@yaak/template-function-cookie",
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
},
|
},
|
||||||
|
"plugins/template-function-datetime": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"date-fns": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"plugins/template-function-encode": {
|
"plugins/template-function-encode": {
|
||||||
"name": "@yaak/template-function-encode",
|
"name": "@yaak/template-function-encode",
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"plugins/importer-postman",
|
"plugins/importer-postman",
|
||||||
"plugins/importer-yaak",
|
"plugins/importer-yaak",
|
||||||
"plugins/template-function-cookie",
|
"plugins/template-function-cookie",
|
||||||
|
"plugins/template-function-datetime",
|
||||||
"plugins/template-function-encode",
|
"plugins/template-function-encode",
|
||||||
"plugins/template-function-fs",
|
"plugins/template-function-fs",
|
||||||
"plugins/template-function-hash",
|
"plugins/template-function-hash",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key
|
|||||||
|
|
||||||
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
||||||
|
|
||||||
export type CallTemplateFunctionResponse = { value: string | null, };
|
export type CallTemplateFunctionResponse = { value: string | null, error?: string, };
|
||||||
|
|
||||||
export type CloseWindowRequest = { label: string, };
|
export type CloseWindowRequest = { label: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -308,15 +308,27 @@ 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);
|
applyFormInputDefaults(fn.args, payload.args.values);
|
||||||
const result = await fn.onRender(ctx, payload.args);
|
try {
|
||||||
this.#sendPayload(
|
const result = await fn.onRender(ctx, payload.args);
|
||||||
windowContext,
|
this.#sendPayload(
|
||||||
{
|
windowContext,
|
||||||
type: 'call_template_function_response',
|
{
|
||||||
value: result ?? null,
|
type: 'call_template_function_response',
|
||||||
},
|
value: result ?? null,
|
||||||
replyId,
|
},
|
||||||
);
|
replyId,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this.#sendPayload(
|
||||||
|
windowContext,
|
||||||
|
{
|
||||||
|
type: 'call_template_function_response',
|
||||||
|
value: null,
|
||||||
|
error: `${err}`.replace(/^Error:\s*/g, ''),
|
||||||
|
},
|
||||||
|
replyId,
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
plugins/template-function-datetime/package.json
Executable file
13
plugins/template-function-datetime/package.json
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "@yaak/template-function-datetime",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "yaakcli build",
|
||||||
|
"dev": "yaakcli dev",
|
||||||
|
"lint": "eslint . --ext .ts,.tsx"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"date-fns": "^4.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
156
plugins/template-function-datetime/src/index.ts
Executable file
156
plugins/template-function-datetime/src/index.ts
Executable file
@@ -0,0 +1,156 @@
|
|||||||
|
import type { TemplateFunctionArg } from '@yaakapp-internal/plugins';
|
||||||
|
import type { PluginDefinition } from '@yaakapp/api';
|
||||||
|
|
||||||
|
import {
|
||||||
|
addDays,
|
||||||
|
addHours,
|
||||||
|
addMinutes,
|
||||||
|
addMonths,
|
||||||
|
addSeconds,
|
||||||
|
addYears,
|
||||||
|
format as formatDate,
|
||||||
|
isValid,
|
||||||
|
parseISO,
|
||||||
|
subDays,
|
||||||
|
subHours,
|
||||||
|
subMinutes,
|
||||||
|
subMonths,
|
||||||
|
subSeconds,
|
||||||
|
subYears,
|
||||||
|
} from 'date-fns';
|
||||||
|
|
||||||
|
const dateArg: TemplateFunctionArg = {
|
||||||
|
type: 'text',
|
||||||
|
name: 'date',
|
||||||
|
label: 'Timestamp',
|
||||||
|
optional: true,
|
||||||
|
defaultValue: '${[ timestamp.iso8601() ]}',
|
||||||
|
description: 'Can be a timestamp in milliseconds, ISO string, or anything parseable by JS `new Date()`',
|
||||||
|
placeholder: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const expressionArg: TemplateFunctionArg = {
|
||||||
|
type: 'text',
|
||||||
|
name: 'expression',
|
||||||
|
label: 'Expression',
|
||||||
|
description: "Modification expression (eg. '-5d +2h 3m'). Available units: y, M, d, h, m, s",
|
||||||
|
optional: true,
|
||||||
|
placeholder: '-5d +2h 3m',
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatArg: TemplateFunctionArg = {
|
||||||
|
name: 'format',
|
||||||
|
label: 'Format String',
|
||||||
|
description: "Format string to describe the output (eg. 'yyyy-MM-dd at HH:mm:ss')",
|
||||||
|
optional: true,
|
||||||
|
placeholder: 'yyyy-MM-dd HH:mm:ss',
|
||||||
|
type: 'text',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const plugin: PluginDefinition = {
|
||||||
|
templateFunctions: [
|
||||||
|
{
|
||||||
|
name: 'timestamp.unix',
|
||||||
|
description: 'Get the current timestamp in seconds',
|
||||||
|
args: [],
|
||||||
|
onRender: async () => String(Math.floor(Date.now() / 1000)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timestamp.unixMillis',
|
||||||
|
description: 'Get the current timestamp in milliseconds',
|
||||||
|
args: [],
|
||||||
|
onRender: async () => String(Date.now()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timestamp.iso8601',
|
||||||
|
description: 'Get the current date in ISO8601 format',
|
||||||
|
args: [],
|
||||||
|
onRender: async () => new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timestamp.format',
|
||||||
|
description: 'Format a date using a dayjs-compatible format string',
|
||||||
|
args: [dateArg, formatArg],
|
||||||
|
onRender: async (_ctx, args) => formatDatetime(args.values),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timestamp.offset',
|
||||||
|
description: 'Get the offset of a date based on an expression',
|
||||||
|
args: [dateArg, expressionArg],
|
||||||
|
onRender: async (_ctx, args) => calculateDatetime(args.values),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function applyDateOp(d: Date, sign: string, amount: number, unit: string): Date {
|
||||||
|
switch (unit) {
|
||||||
|
case 'y':
|
||||||
|
return sign === '-' ? subYears(d, amount) : addYears(d, amount);
|
||||||
|
case 'M':
|
||||||
|
return sign === '-' ? subMonths(d, amount) : addMonths(d, amount);
|
||||||
|
case 'd':
|
||||||
|
return sign === '-' ? subDays(d, amount) : addDays(d, amount);
|
||||||
|
case 'h':
|
||||||
|
return sign === '-' ? subHours(d, amount) : addHours(d, amount);
|
||||||
|
case 'm':
|
||||||
|
return sign === '-' ? subMinutes(d, amount) : addMinutes(d, amount);
|
||||||
|
case 's':
|
||||||
|
return sign === '-' ? subSeconds(d, amount) : addSeconds(d, amount);
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid data calculation unit: ${unit}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOp(op: string): { sign: string; amount: number; unit: string } | null {
|
||||||
|
const match = op.match(/^([+-]?)(\d+)([yMdhms])$/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Invalid date expression: ${op}`);
|
||||||
|
}
|
||||||
|
const [, sign, amount, unit] = match;
|
||||||
|
if (!unit) return null;
|
||||||
|
return { sign: sign ?? '+', amount: Number(amount ?? 0), unit };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDateString(date: string): Date {
|
||||||
|
if (!date.trim()) {
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isoDate = parseISO(date);
|
||||||
|
if (isValid(isoDate)) {
|
||||||
|
return isoDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsDate = /^\d+(\.\d+)?$/.test(date) ? new Date(Number(date)) : new Date(date);
|
||||||
|
if (isValid(jsDate)) {
|
||||||
|
return jsDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Invalid date: ${date}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateDatetime(args: { date?: string; expression?: string }): string {
|
||||||
|
const { date, expression } = args;
|
||||||
|
let jsDate = parseDateString(date ?? '');
|
||||||
|
|
||||||
|
if (expression) {
|
||||||
|
const ops = String(expression)
|
||||||
|
.split(' ')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
for (const op of ops) {
|
||||||
|
const parsed = parseOp(op);
|
||||||
|
if (parsed) {
|
||||||
|
jsDate = applyDateOp(jsDate, parsed.sign, parsed.amount, parsed.unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsDate.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDatetime(args: { date?: string; format?: string }): string {
|
||||||
|
const { date, format = 'yyyy-MM-dd HH:mm:ss' } = args;
|
||||||
|
const d = parseDateString(date ?? '');
|
||||||
|
return formatDate(d, String(format));
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { calculateDatetime, formatDatetime } from '../src';
|
||||||
|
|
||||||
|
describe('formatDatetime', () => {
|
||||||
|
it('returns formatted current date', () => {
|
||||||
|
const result = formatDatetime({});
|
||||||
|
expect(result).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns formatted specific date', () => {
|
||||||
|
const result = formatDatetime({ date: '2025-07-13T12:34:56' });
|
||||||
|
expect(result).toBe('2025-07-13 12:34:56');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns formatted specific timestamp', () => {
|
||||||
|
const result = formatDatetime({ date: '1752435296000' });
|
||||||
|
expect(result).toBe('2025-07-13 12:34:56');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns formatted specific timestamp with decimals', () => {
|
||||||
|
const result = formatDatetime({ date: '1752435296000.19' });
|
||||||
|
expect(result).toBe('2025-07-13 12:34:56');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns formatted date with custom output', () => {
|
||||||
|
const result = formatDatetime({ date: '2025-07-13T12:34:56', format: 'dd/MM/yyyy' });
|
||||||
|
expect(result).toBe('13/07/2025');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles invalid date gracefully', () => {
|
||||||
|
expect(() => formatDatetime({ date: 'invalid-date' })).toThrow('Invalid date: invalid-date');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('calculateDatetime', () => {
|
||||||
|
it('returns ISO string for current date', () => {
|
||||||
|
const result = calculateDatetime({});
|
||||||
|
expect(result).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns ISO string for specific date', () => {
|
||||||
|
const result = calculateDatetime({ date: '2025-07-13T12:34:56Z' });
|
||||||
|
expect(result).toBe('2025-07-13T12:34:56.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies calc operations', () => {
|
||||||
|
const result = calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: '+1d 2h' });
|
||||||
|
expect(result).toBe('2025-07-14T14:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies negative calc operations', () => {
|
||||||
|
const result = calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: '-1d -2h 1m' });
|
||||||
|
expect(result).toBe('2025-07-12T10:01:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for invalid unit', () => {
|
||||||
|
expect(() => calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: '+1x' })).toThrow(
|
||||||
|
'Invalid date expression: +1x',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('throws error for invalid unit weird', () => {
|
||||||
|
expect(() => calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: '+1&#^%' })).toThrow(
|
||||||
|
'Invalid date expression: +1&#^%',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('throws error for bad expression', () => {
|
||||||
|
expect(() =>
|
||||||
|
calculateDatetime({ date: '2025-07-13T12:00:00Z', expression: 'bad expr' }),
|
||||||
|
).toThrow('Invalid date expression: bad');
|
||||||
|
});
|
||||||
|
});
|
||||||
3
plugins/template-function-datetime/tsconfig.json
Normal file
3
plugins/template-function-datetime/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json"
|
||||||
|
}
|
||||||
@@ -53,6 +53,7 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
|
|||||||
"import-data" => {
|
"import-data" => {
|
||||||
let mut file_path = query_map.get("path").map(|s| s.to_owned());
|
let mut file_path = query_map.get("path").map(|s| s.to_owned());
|
||||||
let name = query_map.get("name").map(|s| s.to_owned()).unwrap_or("data".to_string());
|
let name = query_map.get("name").map(|s| s.to_owned()).unwrap_or("data".to_string());
|
||||||
|
_ = window.set_focus();
|
||||||
|
|
||||||
if let Some(file_url) = query_map.get("url") {
|
if let Some(file_url) = query_map.get("url") {
|
||||||
let confirmed_import = app_handle
|
let confirmed_import = app_handle
|
||||||
@@ -96,7 +97,6 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let results = import_data(window, &file_path).await?;
|
let results = import_data(window, &file_path).await?;
|
||||||
_ = window.set_focus();
|
|
||||||
window.emit(
|
window.emit(
|
||||||
"show_toast",
|
"show_toast",
|
||||||
ShowToastRequest {
|
ShowToastRequest {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key
|
|||||||
|
|
||||||
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
||||||
|
|
||||||
export type CallTemplateFunctionResponse = { value: string | null, };
|
export type CallTemplateFunctionResponse = { value: string | null, error?: string, };
|
||||||
|
|
||||||
export type CloseWindowRequest = { label: string, };
|
export type CloseWindowRequest = { label: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -974,6 +974,8 @@ pub struct CallTemplateFunctionRequest {
|
|||||||
#[ts(export, export_to = "gen_events.ts")]
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
pub struct CallTemplateFunctionResponse {
|
pub struct CallTemplateFunctionResponse {
|
||||||
pub value: Option<String>,
|
pub value: Option<String>,
|
||||||
|
#[ts(optional)]
|
||||||
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ use crate::events::{
|
|||||||
BootRequest, CallGrpcRequestActionRequest, CallHttpAuthenticationActionArgs,
|
BootRequest, CallGrpcRequestActionRequest, CallHttpAuthenticationActionArgs,
|
||||||
CallHttpAuthenticationActionRequest, CallHttpAuthenticationRequest,
|
CallHttpAuthenticationActionRequest, CallHttpAuthenticationRequest,
|
||||||
CallHttpAuthenticationResponse, CallHttpRequestActionRequest, CallTemplateFunctionArgs,
|
CallHttpAuthenticationResponse, CallHttpRequestActionRequest, CallTemplateFunctionArgs,
|
||||||
CallTemplateFunctionRequest, CallTemplateFunctionResponse, EmptyPayload, FilterRequest,
|
CallTemplateFunctionRequest, CallTemplateFunctionResponse, EmptyPayload, ErrorResponse,
|
||||||
FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigRequest,
|
FilterRequest, FilterResponse, GetGrpcRequestActionsResponse,
|
||||||
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
|
GetHttpAuthenticationConfigRequest, GetHttpAuthenticationConfigResponse,
|
||||||
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, GetThemesRequest,
|
GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse,
|
||||||
GetThemesResponse, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload,
|
GetTemplateFunctionsResponse, GetThemesRequest, GetThemesResponse, ImportRequest,
|
||||||
JsonPrimitive, PluginWindowContext, RenderPurpose,
|
ImportResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext,
|
||||||
|
RenderPurpose,
|
||||||
};
|
};
|
||||||
use crate::native_template_functions::template_function_secure;
|
use crate::native_template_functions::template_function_secure;
|
||||||
use crate::nodejs::start_nodejs_plugin_runtime;
|
use crate::nodejs::start_nodejs_plugin_runtime;
|
||||||
@@ -644,7 +645,7 @@ impl PluginManager {
|
|||||||
info!("Not applying disabled auth {:?}", auth_name);
|
info!("Not applying disabled auth {:?}", auth_name);
|
||||||
return Ok(CallHttpAuthenticationResponse {
|
return Ok(CallHttpAuthenticationResponse {
|
||||||
set_headers: None,
|
set_headers: None,
|
||||||
set_query_parameters: None
|
set_query_parameters: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,16 +690,25 @@ impl PluginManager {
|
|||||||
.map_err(|e| RenderError(format!("Failed to call template function {e:}")))?;
|
.map_err(|e| RenderError(format!("Failed to call template function {e:}")))?;
|
||||||
|
|
||||||
let value = events.into_iter().find_map(|e| match e.payload {
|
let value = events.into_iter().find_map(|e| match e.payload {
|
||||||
|
// Error returned
|
||||||
|
InternalEventPayload::CallTemplateFunctionResponse(CallTemplateFunctionResponse {
|
||||||
|
error: Some(error),
|
||||||
|
..
|
||||||
|
}) => Some(Err(error)),
|
||||||
|
// Value or null returned
|
||||||
InternalEventPayload::CallTemplateFunctionResponse(CallTemplateFunctionResponse {
|
InternalEventPayload::CallTemplateFunctionResponse(CallTemplateFunctionResponse {
|
||||||
value,
|
value,
|
||||||
}) => Some(value),
|
..
|
||||||
|
}) => Some(Ok(value.unwrap_or_default())),
|
||||||
|
// Generic error returned
|
||||||
|
InternalEventPayload::ErrorResponse(ErrorResponse { error }) => Some(Err(error)),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
match value {
|
match value {
|
||||||
None => Err(RenderError(format!("Template function {fn_name}(…) not found "))),
|
None => Err(RenderError(format!("Template function {fn_name}(…) not found "))),
|
||||||
Some(Some(v)) => Ok(v), // Plugin returned string
|
Some(Ok(v)) => Ok(v),
|
||||||
Some(None) => Ok("".to_string()), // Plugin returned null
|
Some(Err(e)) => Err(RenderError(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import {
|
|||||||
import { useToggle } from '../hooks/useToggle';
|
import { useToggle } from '../hooks/useToggle';
|
||||||
import { convertTemplateToInsecure } from '../lib/encryption';
|
import { convertTemplateToInsecure } from '../lib/encryption';
|
||||||
import { setupOrConfigureEncryption } from '../lib/setupOrConfigureEncryption';
|
import { setupOrConfigureEncryption } from '../lib/setupOrConfigureEncryption';
|
||||||
import { Banner } from './core/Banner';
|
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import { InlineCode } from './core/InlineCode';
|
import { InlineCode } from './core/InlineCode';
|
||||||
|
import { LoadingIcon } from './core/LoadingIcon';
|
||||||
import { PlainInput } from './core/PlainInput';
|
import { PlainInput } from './core/PlainInput';
|
||||||
import { HStack, VStack } from './core/Stacks';
|
import { HStack, VStack } from './core/Stacks';
|
||||||
import { DYNAMIC_FORM_NULL_ARG, DynamicForm } from './DynamicForm';
|
import { DYNAMIC_FORM_NULL_ARG, DynamicForm } from './DynamicForm';
|
||||||
@@ -178,7 +178,10 @@ function InitializedTemplateFunctionDialog({
|
|||||||
{enablePreview && (
|
{enablePreview && (
|
||||||
<VStack className="w-full" space={1}>
|
<VStack className="w-full" space={1}>
|
||||||
<HStack space={0.5}>
|
<HStack space={0.5}>
|
||||||
<div className="text-sm text-text-subtle">Rendered Preview</div>
|
<HStack className="text-sm text-text-subtle" space={1.5}>
|
||||||
|
Rendered Preview
|
||||||
|
{rendered.isPending && <LoadingIcon size="xs" />}
|
||||||
|
</HStack>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="xs"
|
size="xs"
|
||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
@@ -191,26 +194,24 @@ function InitializedTemplateFunctionDialog({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
{rendered.error || tagText.error ? (
|
<InlineCode
|
||||||
<Banner color="danger">{`${rendered.error || tagText.error}`}</Banner>
|
className={classNames(
|
||||||
) : (
|
'whitespace-pre-wrap select-text cursor-text max-h-[10rem] overflow-y-auto hide-scrollbars',
|
||||||
<InlineCode
|
tooLarge && 'italic text-danger',
|
||||||
className={classNames(
|
)}
|
||||||
'whitespace-pre 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: /, '')}
|
||||||
{dataContainsSecrets && !showSecretsInPreview ? (
|
</em>
|
||||||
<span className="italic text-text-subtle">
|
) : dataContainsSecrets && !showSecretsInPreview ? (
|
||||||
------ sensitive values hidden ------
|
<span className="italic text-text-subtle">------ sensitive values hidden ------</span>
|
||||||
</span>
|
) : tooLarge ? (
|
||||||
) : tooLarge ? (
|
'too large to preview'
|
||||||
'too large to preview'
|
) : (
|
||||||
) : (
|
rendered.data || <> </>
|
||||||
rendered.data || <> </>
|
)}
|
||||||
)}
|
</InlineCode>
|
||||||
</InlineCode>
|
|
||||||
)}
|
|
||||||
</VStack>
|
</VStack>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-stretch w-full flex-grow gap-2 [&>*]:flex-1">
|
<div className="flex justify-stretch w-full flex-grow gap-2 [&>*]:flex-1">
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
const show = () =>
|
const show = () =>
|
||||||
showDialog({
|
showDialog({
|
||||||
id: 'template-function-' + Math.random(), // Allow multiple at once
|
id: 'template-function-' + Math.random(), // Allow multiple at once
|
||||||
size: 'sm',
|
size: 'md',
|
||||||
title: <InlineCode>{fn.name}(…)</InlineCode>,
|
title: <InlineCode>{fn.name}(…)</InlineCode>,
|
||||||
description: fn.description,
|
description: fn.description,
|
||||||
render: ({ hide }) => (
|
render: ({ hide }) => (
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export const Icon = memo(function Icon({
|
|||||||
title={title}
|
title={title}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
!spin && 'transform-cpu',
|
!spin && 'transform-gpu',
|
||||||
spin && 'animate-spin',
|
spin && 'animate-spin',
|
||||||
'flex-shrink-0',
|
'flex-shrink-0',
|
||||||
size === 'xl' && 'h-6 w-6',
|
size === 'xl' && 'h-6 w-6',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type Props = Omit<TooltipProps, 'children'> & {
|
|||||||
iconSize?: IconProps['size'];
|
iconSize?: IconProps['size'];
|
||||||
iconColor?: IconProps['color'];
|
iconColor?: IconProps['color'];
|
||||||
className?: string;
|
className?: string;
|
||||||
|
tabIndex?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function IconTooltip({
|
export function IconTooltip({
|
||||||
|
|||||||
@@ -519,7 +519,7 @@ function EncryptionInput({
|
|||||||
color="danger"
|
color="danger"
|
||||||
size={props.size}
|
size={props.size}
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
rightSlot={<IconTooltip content={state.error} icon="alert_triangle" />}
|
rightSlot={<IconTooltip tabIndex={-1} content={state.error} icon="alert_triangle" />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setupOrConfigureEncryption();
|
setupOrConfigureEncryption();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function Label({
|
|||||||
({tag})
|
({tag})
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{help && <IconTooltip content={help} />}
|
{help && <IconTooltip tabIndex={-1} content={help} />}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { minPromiseMillis } from '../lib/minPromiseMillis';
|
||||||
import { invokeCmd } from '../lib/tauri';
|
import { invokeCmd } from '../lib/tauri';
|
||||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||||
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
|
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
|
||||||
@@ -8,10 +9,9 @@ export function useRenderTemplate(template: string) {
|
|||||||
const workspaceId = useAtomValue(activeWorkspaceIdAtom) ?? 'n/a';
|
const workspaceId = useAtomValue(activeWorkspaceIdAtom) ?? 'n/a';
|
||||||
const environmentId = useActiveEnvironment()?.id ?? null;
|
const environmentId = useActiveEnvironment()?.id ?? null;
|
||||||
return useQuery<string>({
|
return useQuery<string>({
|
||||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
queryKey: ['render_template', template, workspaceId, environmentId],
|
queryKey: ['render_template', template, workspaceId, environmentId],
|
||||||
queryFn: () => renderTemplate({ template, workspaceId, environmentId }),
|
queryFn: () => minPromiseMillis(renderTemplate({ template, workspaceId, environmentId }), 200),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { invokeCmd } from '../lib/tauri';
|
|||||||
|
|
||||||
export function useTemplateTokensToString(tokens: Tokens) {
|
export function useTemplateTokensToString(tokens: Tokens) {
|
||||||
return useQuery<string>({
|
return useQuery<string>({
|
||||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
queryKey: ['template_tokens_to_string', tokens],
|
queryKey: ['template_tokens_to_string', tokens],
|
||||||
queryFn: () => templateTokensToString(tokens),
|
queryFn: () => templateTokensToString(tokens),
|
||||||
|
|||||||
Reference in New Issue
Block a user