diff --git a/app/__test__/flows/SubWorkflowTask.spec.ts b/app/__test__/flows/SubWorkflowTask.spec.ts index c52a0a23..e43473a7 100644 --- a/app/__test__/flows/SubWorkflowTask.spec.ts +++ b/app/__test__/flows/SubWorkflowTask.spec.ts @@ -1,5 +1,23 @@ import { describe, expect, test } from "bun:test"; -import { Flow, LogTask, RenderTask, SubFlowTask } from "../../src/flows"; +import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows"; +import { Type } from "@sinclair/typebox"; + +export class StringifyTask extends Task< + typeof StringifyTask.schema, + Output +> { + type = "stringify"; + + static override schema = Type.Optional( + Type.Object({ + input: Type.Optional(Type.String()), + }), + ); + + async execute() { + return JSON.stringify(this.params.input) as Output; + } +} describe("SubFlowTask", async () => { test("Simple Subflow", async () => { @@ -22,8 +40,6 @@ describe("SubFlowTask", async () => { const execution = flow.createExecution(); await execution.start(); - /*console.log(execution.logs); - console.log(execution.getResponse());*/ expect(execution.getResponse()).toEqual("Subflow output: subflow"); }); @@ -40,8 +56,8 @@ describe("SubFlowTask", async () => { loop: true, input: [1, 2, 3], }); - const task3 = new RenderTask("render2", { - render: `Subflow output: {{ sub.output | join: ", " }}`, + const task3 = new StringifyTask("stringify", { + input: "{{ sub.output }}", }); const flow = new Flow("test", [task, task2, task3], []); @@ -51,41 +67,6 @@ describe("SubFlowTask", async () => { const execution = flow.createExecution(); await execution.start(); - console.log("errors", execution.getErrors()); - - /*console.log(execution.logs); - console.log(execution.getResponse());*/ - - expect(execution.getResponse()).toEqual("Subflow output: run 1, run 2, run 3"); - }); - - test("Simple loop from flow input", async () => { - const subTask = new RenderTask("render", { - render: "run {{ flow.output }}", - }); - - const subflow = new Flow("subflow", [subTask]); - - const task = new LogTask("log"); - const task2 = new SubFlowTask("sub", { - flow: subflow, - loop: true, - input: "{{ flow.output | json }}", - }); - const task3 = new RenderTask("render2", { - render: `Subflow output: {{ sub.output | join: ", " }}`, - }); - - const flow = new Flow("test", [task, task2, task3], []); - flow.task(task).asInputFor(task2); - flow.task(task2).asInputFor(task3); - - const execution = flow.createExecution(); - await execution.start([4, 5, 6]); - - /*console.log(execution.logs); - console.log(execution.getResponse());*/ - - expect(execution.getResponse()).toEqual("Subflow output: run 4, run 5, run 6"); + expect(execution.getResponse()).toEqual('"run 1,run 2,run 3"'); }); }); diff --git a/app/__test__/flows/Task.spec.ts b/app/__test__/flows/Task.spec.ts index a7da9836..4acbe78a 100644 --- a/app/__test__/flows/Task.spec.ts +++ b/app/__test__/flows/Task.spec.ts @@ -51,62 +51,4 @@ describe("Task", async () => { expect(result.test).toEqual({ key: "path", value: "1/1" }); }); - - test("resolveParams: with json", async () => { - const result = await Task.resolveParams( - Type.Object({ - test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })), - }), - { - test: "{{ some | json }}", - }, - { - some: { - key: "path", - value: "1/1", - }, - }, - ); - - expect(result.test).toEqual({ key: "path", value: "1/1" }); - }); - - test("resolveParams: with array", async () => { - const result = await Task.resolveParams( - Type.Object({ - test: dynamic(Type.Array(Type.String())), - }), - { - test: '{{ "1,2,3" | split: "," | json }}', - }, - ); - - expect(result.test).toEqual(["1", "2", "3"]); - }); - - test("resolveParams: boolean", async () => { - const result = await Task.resolveParams( - Type.Object({ - test: dynamic(Type.Boolean()), - }), - { - test: "{{ true }}", - }, - ); - - expect(result.test).toEqual(true); - }); - - test("resolveParams: float", async () => { - const result = await Task.resolveParams( - Type.Object({ - test: dynamic(Type.Number(), Number.parseFloat), - }), - { - test: "{{ 3.14 }}", - }, - ); - - expect(result.test).toEqual(3.14); - }); }); diff --git a/app/src/core/template/SimpleRenderer.spec.ts b/app/src/core/template/SimpleRenderer.spec.ts new file mode 100644 index 00000000..6f922f36 --- /dev/null +++ b/app/src/core/template/SimpleRenderer.spec.ts @@ -0,0 +1,70 @@ +import { describe, expect, test } from "bun:test"; +import { SimpleRenderer } from "core"; + +describe(SimpleRenderer, () => { + const renderer = new SimpleRenderer( + { + name: "World", + views: 123, + nested: { + foo: "bar", + baz: ["quz", "foo"], + }, + someArray: [1, 2, 3], + enabled: true, + }, + { + renderKeys: true, + }, + ); + + test("strings", async () => { + const tests = [ + ["Hello {{ name }}, count: {{views}}", "Hello World, count: 123"], + ["Nested: {{nested.foo}}", "Nested: bar"], + ["Nested: {{nested.baz[0]}}", "Nested: quz"], + ] as const; + + for (const [template, expected] of tests) { + expect(await renderer.renderString(template)).toEqual(expected); + } + }); + + test("arrays", async () => { + const tests = [ + [ + ["{{someArray[0]}}", "{{someArray[1]}}", "{{someArray[2]}}"], + ["1", "2", "3"], + ], + ] as const; + + for (const [template, expected] of tests) { + const result = await renderer.render(template); + expect(result).toEqual(expected as any); + } + }); + + test("objects", async () => { + const tests = [ + [ + { + foo: "{{name}}", + bar: "{{views}}", + baz: "{{nested.foo}}", + quz: "{{nested.baz[0]}}", + }, + { + foo: "World", + bar: "123", + baz: "bar", + quz: "quz", + }, + ], + ] as const; + + for (const [template, expected] of tests) { + const result = await renderer.render(template); + expect(result).toEqual(expected as any); + } + }); +}); diff --git a/app/src/core/template/SimpleRenderer.ts b/app/src/core/template/SimpleRenderer.ts index 361ebe4c..b3bf6aaf 100644 --- a/app/src/core/template/SimpleRenderer.ts +++ b/app/src/core/template/SimpleRenderer.ts @@ -1,17 +1,13 @@ -import { Liquid, LiquidError } from "liquidjs"; -import type { RenderOptions } from "liquidjs/dist/liquid-options"; -import { BkndError } from "../errors"; +import { get } from "lodash-es"; export type TemplateObject = Record>; -export type TemplateTypes = string | TemplateObject; +export type TemplateTypes = string | TemplateObject | any; -export type SimpleRendererOptions = RenderOptions & { +export type SimpleRendererOptions = { renderKeys?: boolean; }; export class SimpleRenderer { - private engine = new Liquid(); - constructor( private variables: Record = {}, private options: SimpleRendererOptions = {}, @@ -33,44 +29,29 @@ export class SimpleRenderer { flat = String(template); } - const checks = ["{{", "{%", "{#", "{:"]; + const checks = ["{{"]; return checks.some((check) => flat.includes(check)); } - async render(template: Given): Promise { - try { - if (typeof template === "string") { - return (await this.renderString(template)) as unknown as Given; - } else if (Array.isArray(template)) { - return (await Promise.all( - template.map((item) => this.render(item)), - )) as unknown as Given; - } else if (typeof template === "object") { - return (await this.renderObject(template)) as unknown as Given; - } - } catch (e) { - if (e instanceof LiquidError) { - const details = { - name: e.name, - token: { - kind: e.token.kind, - input: e.token.input, - begin: e.token.begin, - end: e.token.end, - }, - }; + async render(template: Given): Promise { + if (typeof template === "undefined" || template === null) return template; - throw new BkndError(e.message, details, "liquid"); - } - - throw e; + if (typeof template === "string") { + return (await this.renderString(template)) as unknown as Given; + } else if (Array.isArray(template)) { + return (await Promise.all(template.map((item) => this.render(item)))) as unknown as Given; + } else if (typeof template === "object") { + return (await this.renderObject(template as any)) as unknown as Given; } throw new Error("Invalid template type"); } async renderString(template: string): Promise { - return this.engine.parseAndRender(template, this.variables, this.options); + return template.replace(/{{\s*([^{}]+?)\s*}}/g, (_, expr: string) => { + const value = get(this.variables, expr.trim()); + return value == null ? "" : String(value); + }); } async renderObject(template: TemplateObject): Promise { diff --git a/app/src/flows/tasks/Task.tsx b/app/src/flows/tasks/Task.tsx index 933bf15f..e035af92 100644 --- a/app/src/flows/tasks/Task.tsx +++ b/app/src/flows/tasks/Task.tsx @@ -106,7 +106,7 @@ export abstract class Task { inputs: object = {}, ): Promise> { const newParams: any = {}; - const renderer = new SimpleRenderer(inputs, { strictVariables: true, renderKeys: true }); + const renderer = new SimpleRenderer(inputs, { renderKeys: true }); for (const [key, value] of Object.entries(params)) { if (value && SimpleRenderer.hasMarkup(value)) { diff --git a/app/src/ui/components/code/CodeEditor.tsx b/app/src/ui/components/code/CodeEditor.tsx index 480cb790..6c814349 100644 --- a/app/src/ui/components/code/CodeEditor.tsx +++ b/app/src/ui/components/code/CodeEditor.tsx @@ -1,12 +1,12 @@ import { default as CodeMirror, type ReactCodeMirrorProps } from "@uiw/react-codemirror"; import { json } from "@codemirror/lang-json"; -import { type LiquidCompletionConfig, liquid } from "@codemirror/lang-liquid"; +import { html } from "@codemirror/lang-html"; import { useTheme } from "ui/client/use-theme"; export type CodeEditorProps = ReactCodeMirrorProps & { _extensions?: Partial<{ json: boolean; - liquid: LiquidCompletionConfig; + html: boolean; }>; }; @@ -31,7 +31,8 @@ export default function CodeEditor({ case "json": return json(); case "liquid": - return liquid(config); + case "html": + return html(config); } return undefined; }) diff --git a/app/src/ui/components/code/HtmlEditor.tsx b/app/src/ui/components/code/HtmlEditor.tsx new file mode 100644 index 00000000..1716f1cf --- /dev/null +++ b/app/src/ui/components/code/HtmlEditor.tsx @@ -0,0 +1,23 @@ +import { Suspense, lazy } from "react"; +import { twMerge } from "tailwind-merge"; + +import type { CodeEditorProps } from "./CodeEditor"; +const CodeEditor = lazy(() => import("./CodeEditor")); + +export function HtmlEditor({ editable, ...props }: CodeEditorProps) { + return ( + + + + ); +} diff --git a/app/src/ui/components/code/LiquidJsEditor.tsx b/app/src/ui/components/code/LiquidJsEditor.tsx deleted file mode 100644 index d73c6f1c..00000000 --- a/app/src/ui/components/code/LiquidJsEditor.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { Suspense, lazy } from "react"; -import { twMerge } from "tailwind-merge"; - -import type { CodeEditorProps } from "./CodeEditor"; -const CodeEditor = lazy(() => import("./CodeEditor")); - -const filters = [ - { label: "abs" }, - { label: "append" }, - { label: "array_to_sentence_string" }, - { label: "at_least" }, - { label: "at_most" }, - { label: "capitalize" }, - { label: "ceil" }, - { label: "cgi_escape" }, - { label: "compact" }, - { label: "concat" }, - { label: "date" }, - { label: "date_to_long_string" }, - { label: "date_to_rfc822" }, - { label: "date_to_string" }, - { label: "date_to_xmlschema" }, - { label: "default" }, - { label: "divided_by" }, - { label: "downcase" }, - { label: "escape" }, - { label: "escape_once" }, - { label: "find" }, - { label: "find_exp" }, - { label: "first" }, - { label: "floor" }, - { label: "group_by" }, - { label: "group_by_exp" }, - { label: "inspect" }, - { label: "join" }, - { label: "json" }, - { label: "jsonify" }, - { label: "last" }, - { label: "lstrip" }, - { label: "map" }, - { label: "minus" }, - { label: "modulo" }, - { label: "newline_to_br" }, - { label: "normalize_whitespace" }, - { label: "number_of_words" }, - { label: "plus" }, - { label: "pop" }, - { label: "push" }, - { label: "prepend" }, - { label: "raw" }, - { label: "remove" }, - { label: "remove_first" }, - { label: "remove_last" }, - { label: "replace" }, - { label: "replace_first" }, - { label: "replace_last" }, - { label: "reverse" }, - { label: "round" }, - { label: "rstrip" }, - { label: "shift" }, - { label: "size" }, - { label: "slice" }, - { label: "slugify" }, - { label: "sort" }, - { label: "sort_natural" }, - { label: "split" }, - { label: "strip" }, - { label: "strip_html" }, - { label: "strip_newlines" }, - { label: "sum" }, - { label: "times" }, - { label: "to_integer" }, - { label: "truncate" }, - { label: "truncatewords" }, - { label: "uniq" }, - { label: "unshift" }, - { label: "upcase" }, - { label: "uri_escape" }, - { label: "url_decode" }, - { label: "url_encode" }, - { label: "where" }, - { label: "where_exp" }, - { label: "xml_escape" }, -]; - -const tags = [ - { label: "assign" }, - { label: "capture" }, - { label: "case" }, - { label: "comment" }, - { label: "cycle" }, - { label: "decrement" }, - { label: "echo" }, - { label: "else" }, - { label: "elsif" }, - { label: "for" }, - { label: "if" }, - { label: "include" }, - { label: "increment" }, - { label: "layout" }, - { label: "liquid" }, - { label: "raw" }, - { label: "render" }, - { label: "tablerow" }, - { label: "unless" }, - { label: "when" }, -]; - -export function LiquidJsEditor({ editable, ...props }: CodeEditorProps) { - return ( - - - - ); -} diff --git a/app/src/ui/components/form/json-schema/fields/LiquidJsField.tsx b/app/src/ui/components/form/json-schema/fields/HtmlField.tsx similarity index 71% rename from app/src/ui/components/form/json-schema/fields/LiquidJsField.tsx rename to app/src/ui/components/form/json-schema/fields/HtmlField.tsx index 830bb6ba..0bb3a463 100644 --- a/app/src/ui/components/form/json-schema/fields/LiquidJsField.tsx +++ b/app/src/ui/components/form/json-schema/fields/HtmlField.tsx @@ -1,9 +1,9 @@ import type { FieldProps } from "@rjsf/utils"; -import { LiquidJsEditor } from "../../../code/LiquidJsEditor"; import { Label } from "../templates/FieldTemplate"; +import { HtmlEditor } from "ui/components/code/HtmlEditor"; // @todo: move editor to lazy loading component -export default function LiquidJsField({ +export default function HtmlField({ formData, onChange, disabled, @@ -20,7 +20,7 @@ export default function LiquidJsField({ return (
); } diff --git a/app/src/ui/components/form/json-schema/fields/index.ts b/app/src/ui/components/form/json-schema/fields/index.ts index 6b194817..6c499c68 100644 --- a/app/src/ui/components/form/json-schema/fields/index.ts +++ b/app/src/ui/components/form/json-schema/fields/index.ts @@ -1,10 +1,10 @@ import JsonField from "./JsonField"; -import LiquidJsField from "./LiquidJsField"; import MultiSchemaField from "./MultiSchemaField"; +import HtmlField from "./HtmlField"; export const fields = { AnyOfField: MultiSchemaField, OneOfField: MultiSchemaField, JsonField, - LiquidJsField, + HtmlField, }; diff --git a/app/src/ui/modules/flows/components/tasks/RenderTaskComponent.tsx b/app/src/ui/modules/flows/components/tasks/RenderTaskComponent.tsx index 115fcf8c..ceb8a09b 100644 --- a/app/src/ui/modules/flows/components/tasks/RenderTaskComponent.tsx +++ b/app/src/ui/modules/flows/components/tasks/RenderTaskComponent.tsx @@ -10,7 +10,7 @@ export function RenderTaskComponent(props: TaskComponentProps) { onChange={console.log} uiSchema={{ render: { - "ui:field": "LiquidJsField", + "ui:field": "HtmlField", }, }} /> diff --git a/app/src/ui/modules/flows/components2/nodes/tasks/RenderNode.tsx b/app/src/ui/modules/flows/components2/nodes/tasks/RenderNode.tsx index 106d8bcd..c447473c 100644 --- a/app/src/ui/modules/flows/components2/nodes/tasks/RenderNode.tsx +++ b/app/src/ui/modules/flows/components2/nodes/tasks/RenderNode.tsx @@ -1,12 +1,12 @@ import { IconWorld } from "@tabler/icons-react"; -import { LiquidJsEditor } from "ui/components/code/LiquidJsEditor"; import { BaseNode } from "../BaseNode"; +import { HtmlEditor } from "ui/components/code/HtmlEditor"; export function RenderNode(props) { return (
- +
); diff --git a/app/src/ui/routes/settings/routes/flows.settings.tsx b/app/src/ui/routes/settings/routes/flows.settings.tsx index d0adac5c..0cdfae71 100644 --- a/app/src/ui/routes/settings/routes/flows.settings.tsx +++ b/app/src/ui/routes/settings/routes/flows.settings.tsx @@ -90,7 +90,7 @@ export const FlowsSettings = ({ schema, config }) => { uiSchema={{ params: { render: { - "ui:field": "LiquidJsField", + "ui:field": "HtmlField", }, }, }} diff --git a/app/src/ui/routes/test/tests/liquid-js-test.tsx b/app/src/ui/routes/test/tests/liquid-js-test.tsx index 4f0986b7..382f3f6a 100644 --- a/app/src/ui/routes/test/tests/liquid-js-test.tsx +++ b/app/src/ui/routes/test/tests/liquid-js-test.tsx @@ -1,12 +1,12 @@ import { TextInput } from "@mantine/core"; -import { LiquidJsEditor } from "../../../components/code/LiquidJsEditor"; import * as Formy from "../../../components/form/Formy"; +import { HtmlEditor } from "ui/components/code/HtmlEditor"; export function LiquidJsTest() { return (

LiquidJsTest

- +