diff --git a/src/ui/src/builder/BuilderApp.vue b/src/ui/src/builder/BuilderApp.vue
index eab0913c7..2d3760bc3 100644
--- a/src/ui/src/builder/BuilderApp.vue
+++ b/src/ui/src/builder/BuilderApp.vue
@@ -5,20 +5,12 @@
-
brush
- User Interface
-
-
- code
- Code
- {{
- logEntryCount
- }}
+ UI
let selectedId: Ref = ref(null);
-const selectOption = (optionId: "ui" | "code" | "preview" | "workflows") => {
+const selectOption = (optionId: "ui" | "preview" | "workflows") => {
const preMode = ssbm.getMode();
if (preMode == optionId) return;
selectedId.value = optionId;
ssbm.setMode(optionId);
+ if (optionId == "preview") {
+ ssbm.openPanels.clear();
+ }
if (
optionId == "preview" ||
preMode == "workflows" ||
@@ -60,10 +53,6 @@ const selectOption = (optionId: "ui" | "code" | "preview" | "workflows") => {
};
const activeId = computed(() => ssbm.getMode());
-
-const logEntryCount = computed(() => {
- return ssbm.getLogEntryCount();
-});
diff --git a/src/ui/src/writerTypes.ts b/src/ui/src/writerTypes.ts
index 8704f659d..bbe92a36f 100644
--- a/src/ui/src/writerTypes.ts
+++ b/src/ui/src/writerTypes.ts
@@ -102,8 +102,7 @@ export type WriterComponentDefinition = {
previewField?: string; // Which field to use for previewing in the Component Tree
positionless?: boolean; // Whether this type of component is positionless (like Sidebar)
outs?:
- | Record<"$", { field: string }>
- | Record;
+ Record;
};
export type BuilderManager = ReturnType;
diff --git a/src/writer/app_runner.py b/src/writer/app_runner.py
index d26c08692..73c91016d 100644
--- a/src/writer/app_runner.py
+++ b/src/writer/app_runner.py
@@ -337,7 +337,7 @@ def _execute_user_code(self) -> None:
if captured_stdout:
writer.core.initial_state.add_log_entry(
- "info", "Stdout message during initialisation", captured_stdout)
+ "info", "Stdout message during initialization", captured_stdout)
# Register non-private functions as handlers
self.handler_registry.register_module(writeruserapp)
diff --git a/src/writer/core.py b/src/writer/core.py
index 1387771f2..bd9f68dfd 100644
--- a/src/writer/core.py
+++ b/src/writer/core.py
@@ -12,6 +12,7 @@
import math
import multiprocessing
import numbers
+import os
import re
import secrets
import time
@@ -1445,7 +1446,8 @@ def replacer(matched):
try:
if as_json:
serialised_value = state_serialiser.serialise(expr_value)
- serialised_value = json.dumps(serialised_value)
+ if not isinstance(serialised_value, str):
+ serialised_value = json.dumps(serialised_value)
return serialised_value
return expr_value
except BaseException:
@@ -1581,6 +1583,8 @@ def parse_expression(self, expr: str, instance_path: Optional[InstancePath] = No
return accessors
+ def get_env_variable_value(self, expr: str):
+ return os.getenv(expr[1:])
def evaluate_expression(self, expr: str, instance_path: Optional[InstancePath] = None, base_context = {}) -> Any:
context_data = base_context
@@ -1605,6 +1609,9 @@ def evaluate_expression(self, expr: str, instance_path: Optional[InstancePath] =
if isinstance(result, StateProxy):
return result.to_dict()
+ if result is None and expr.startswith("$"):
+ return self.get_env_variable_value(expr)
+
return result
diff --git a/src/writer/workflows.py b/src/writer/workflows.py
index 5fdf5c548..dbeb99958 100644
--- a/src/writer/workflows.py
+++ b/src/writer/workflows.py
@@ -31,23 +31,25 @@ def run_workflow(session, component_id: str, execution_env: Dict):
if tool and tool.return_value:
return_value = tool.return_value
except BaseException as e:
- _generate_run_log(session, execution, "error")
+ _generate_run_log(session, execution, "Failed workflow execution", "error")
raise e
else:
- _generate_run_log(session, execution, "info", return_value)
+ _generate_run_log(session, execution, "Workflow execution", "info", return_value)
-def _generate_run_log(session: "writer.core.WriterSession", execution: Dict[str, WorkflowBlock], entry_type: Literal["info", "error"], return_value: Optional[Any] = None):
+def _generate_run_log(session: "writer.core.WriterSession", execution: Dict[str, WorkflowBlock], title: str, entry_type: Literal["info", "error"], return_value: Optional[Any] = None):
+ if not writer.core.Config.is_mail_enabled_for_log:
+ return
exec_log = []
for component_id, tool in execution.items():
exec_log.append({
"componentId": component_id,
"outcome": tool.outcome,
- # "outcome": tool.outcome + repr(tool.return_value) + repr(tool.result),
+ "result": tool.result,
"executionTimeInSeconds": tool.execution_time_in_seconds
})
- msg = f"Execution finished with value {repr(return_value)}"
+ msg = "Execution finished."
state = session.session_state
- state.add_log_entry(entry_type, "Workflow execution", msg, workflow_execution=exec_log)
+ state.add_log_entry(entry_type, title, msg, workflow_execution=exec_log)
def get_terminal_nodes(nodes):
diff --git a/src/writer/workflows_blocks/httprequest.py b/src/writer/workflows_blocks/httprequest.py
index 423d6cffd..b61b6c4c2 100644
--- a/src/writer/workflows_blocks/httprequest.py
+++ b/src/writer/workflows_blocks/httprequest.py
@@ -68,7 +68,7 @@ def run(self):
try:
method = self._get_field("method", False, "get")
url = self._get_field("url")
- headers = self._get_field("headers")
+ headers = self._get_field("headers", True)
body = self._get_field("body")
req = requests.request(method, url, headers=headers, data=body)
self.result = {
@@ -80,6 +80,7 @@ def run(self):
self.outcome = "success"
else:
self.outcome = "responseError"
+ raise RuntimeError("HTTP response with code " + str(req.status_code))
except BaseException as e:
self.outcome = "connectionError"
raise e
\ No newline at end of file
diff --git a/src/writer/workflows_blocks/returnvalue.py b/src/writer/workflows_blocks/returnvalue.py
index 89a048cc8..9ccf318e2 100644
--- a/src/writer/workflows_blocks/returnvalue.py
+++ b/src/writer/workflows_blocks/returnvalue.py
@@ -14,7 +14,7 @@ def register(cls, type: str):
writer={
"name": "Return value",
"description": "Returns a value from a workflow or sub-workflow.",
- "category": "Writer",
+ "category": "Logic",
"fields": {
"value": {
"name": "Value",
diff --git a/src/writer/workflows_blocks/writerchat.py b/src/writer/workflows_blocks/writerchat.py
index ac30703d7..5bcabdf38 100644
--- a/src/writer/workflows_blocks/writerchat.py
+++ b/src/writer/workflows_blocks/writerchat.py
@@ -5,6 +5,7 @@
DEFAULT_MODEL = "palmyra-x-004"
+
class WriterChat(WorkflowBlock):
@classmethod
@@ -40,8 +41,11 @@ def register(cls, type: str):
}
},
"outs": {
- "$dynamic": {
- "field": "tools"
+ "tools": {
+ "name": "Tools",
+ "field": "tools",
+ "description": "Run associated tools.",
+ "style": "dynamic"
},
"success": {
"name": "Success",
@@ -74,7 +78,7 @@ def run_branch(self, outcome: str, **args):
def _make_callable(self, tool_name: str):
def callable(**args):
- return self.run_branch(f"$dynamic_{tool_name}", **args)
+ return self.run_branch(f"tools_{tool_name}", **args)
return callable
def run(self):
diff --git a/tests/e2e/package.json b/tests/e2e/package.json
index 0a5ac1e35..c43465658 100644
--- a/tests/e2e/package.json
+++ b/tests/e2e/package.json
@@ -11,7 +11,8 @@
"e2e:chromium": "playwright test --project=chromium",
"e2e:firefox": "playwright test --project=firefox",
"e2e:webkit": "playwright test --project=webkit",
- "e2e:ui": "playwright test --project=chromium --ui"
+ "e2e:ui": "playwright test --project=chromium --ui",
+ "e2e:grep": "playwright test --project=chromium --grep "
},
"dependencies": {
"writer-ui": "*",
diff --git a/tests/e2e/tests/reuse.spec.ts b/tests/e2e/tests/reuse.spec.ts
index 361d2a6c8..73b54eb2c 100644
--- a/tests/e2e/tests/reuse.spec.ts
+++ b/tests/e2e/tests/reuse.spec.ts
@@ -11,9 +11,9 @@ test.describe("Reuse component", () => {
}
const dragNewComponent = async (page: Page, type: string, where = ".CoreSection") => {
- await page
- .locator(`div.component.button[data-component-type="${type}"]`)
- .dragTo(page.locator(where));
+ await page
+ .locator(`div.component.button[data-component-type="${type}"]`)
+ .dragTo(page.locator(where));
}
const getSelectedComponentId = async (page: Page): Promise => {
@@ -44,8 +44,13 @@ test.describe("Reuse component", () => {
return await getSelectedComponentId(page);
};
+ const closeSettingsBar = async (page: Page) => {
+ await page.locator('.BuilderSettings button[data-automation-action="close"]').click();
+ }
+
const removeComponent = async (page: Page, selector: string) => {
await page.locator(".CorePage").click();
+ await closeSettingsBar(page);
await page.locator(selector).click();
await page
.locator(
@@ -66,13 +71,13 @@ test.describe("Reuse component", () => {
test.describe("basic", () => {
let url: string;
- test.beforeAll(async ({request}) => {
+ test.beforeAll(async ({ request }) => {
const response = await request.post(`/preset/section`);
expect(response.ok()).toBeTruthy();
- ({url} = await response.json());
+ ({ url } = await response.json());
});
- test.afterAll(async ({request}) => {
+ test.afterAll(async ({ request }) => {
await request.delete(url);
});
@@ -84,7 +89,7 @@ test.describe("Reuse component", () => {
await removeComponent(page, '.CoreReuse');
});
- test("empty info", async ({page}) => {
+ test("empty info", async ({ page }) => {
await createReuseable(page);
await expect(page.locator(COMPONENT_LOCATOR)).toHaveClass(/empty/);
});
@@ -97,16 +102,19 @@ test.describe("Reuse component", () => {
await expect(page.locator('.CoreReuse.CoreText.component')).toHaveCount(1);
});
- test("self-referencing", async ({page}) => {
+ test("self-referencing", async ({ page }) => {
const id = await createReuseable(page);
await setReuseTarget(page, id);
await expect(page.locator('.CoreReuse.component')).toHaveClass(/invalid-value/);
});
- test("reuse in incorect context", async ({page}) => {
+ test("reuse in incorect context", async ({ page }) => {
const id = await createSidebar(page);
+ await closeSettingsBar(page);
await createReuseable(page);
+ await closeSettingsBar(page);
await setReuseTarget(page, id);
+ await closeSettingsBar(page);
await expect(page.locator(COMPONENT_LOCATOR)).toHaveClass(/invalid-context/);
});
});
@@ -114,13 +122,13 @@ test.describe("Reuse component", () => {
test.describe("between pages", () => {
let url: string;
- test.beforeAll(async ({request}) => {
+ test.beforeAll(async ({ request }) => {
const response = await request.post(`/preset/2pages`);
expect(response.ok()).toBeTruthy();
- ({url} = await response.json());
+ ({ url } = await response.json());
});
- test.afterAll(async ({request}) => {
+ test.afterAll(async ({ request }) => {
await request.delete(url);
});
@@ -129,28 +137,28 @@ test.describe("Reuse component", () => {
});
test.afterEach(async ({ page }) => {
- await page.goto(url+"#page2");
+ await page.goto(url + "#page2");
await removeComponent(page, ".CoreReuse");
- await page.goto(url+"#page1");
+ await page.goto(url + "#page1");
const c = await page.locator('.CoreSidebar').count();
if (c > 0) {
await removeComponent(page, '.CoreSidebar');
}
});
- test("reuse sloted component", async ({page}) => {
- await page.goto(url+"#page1");
+ test("reuse sloted component", async ({ page }) => {
+ await page.goto(url + "#page1");
const id = await createSidebar(page);
- await page.goto(url+"#page2");
+ await page.goto(url + "#page2");
await createReuseable(page, ".CorePage");
await setReuseTarget(page, id);
await expect(page.locator('.sidebarContainer .CoreReuse.CoreSidebar')).toHaveCount(1);
});
- test("dynamic slot change", async ({page}) => {
- await page.goto(url+"#page1");
+ test("dynamic slot change", async ({ page }) => {
+ await page.goto(url + "#page1");
const sidebarId = await createSidebar(page);
- await page.goto(url+"#page2");
+ await page.goto(url + "#page2");
await createReuseable(page, ".CorePage");
await setReuseTarget(page, sidebarId);
await expect(page.locator('.sidebarContainer .CoreReuse.CoreSidebar')).toHaveCount(1);
@@ -160,15 +168,15 @@ test.describe("Reuse component", () => {
expect(page.locator('.main .CoreReuse.CoreText')).toHaveCount(1);
});
- test("target component deleted", async ({page}) => {
- await page.goto(url+"#page1");
+ test("target component deleted", async ({ page }) => {
+ await page.goto(url + "#page1");
const sidebarId = await createSidebar(page);
- await page.goto(url+"#page2");
+ await page.goto(url + "#page2");
await createReuseable(page, ".CorePage");
await setReuseTarget(page, sidebarId);
- await page.goto(url+"#page1");
+ await page.goto(url + "#page1");
await removeComponent(page, '.CoreSidebar');
- await page.goto(url+"#page2");
+ await page.goto(url + "#page2");
await expect(page.locator('.CoreReuse.component')).toHaveClass(/invalid-value/);
});
});
@@ -180,13 +188,13 @@ test.describe("Reuse component", () => {
const COLUMN2 = ".CoreColumns .CoreColumn:nth-child(2 of .CoreColumn)";
let url: string;
- test.beforeAll(async ({request}) => {
+ test.beforeAll(async ({ request }) => {
const response = await request.post(`/preset/2columns`);
expect(response.ok()).toBeTruthy();
- ({url} = await response.json());
+ ({ url } = await response.json());
});
- test.afterAll(async ({request}) => {
+ test.afterAll(async ({ request }) => {
await request.delete(url);
});
@@ -196,6 +204,7 @@ test.describe("Reuse component", () => {
test("create, drag and drop and remove", async ({ page }) => {
await createReuseable(page, COLUMN1);
+ await closeSettingsBar(page);
await moveFromTo(page, COMPONENT_LOCATOR, COLUMN1, COLUMN2);
await removeComponent(page, COMPONENT_LOCATOR);
});
@@ -204,6 +213,7 @@ test.describe("Reuse component", () => {
const id = await createText(page, '.CorePage');
await createReuseable(page, COLUMN1);
await setReuseTarget(page, id);
+ await closeSettingsBar(page);
await moveFromTo(page, COMPONENT_LOCATOR, COLUMN1, COLUMN2);
await removeComponent(page, COMPONENT_LOCATOR);
await removeComponent(page, '.CoreText');
diff --git a/tests/e2e/tests/undoRedo.spec.ts b/tests/e2e/tests/undoRedo.spec.ts
index 072604344..cd758adc6 100644
--- a/tests/e2e/tests/undoRedo.spec.ts
+++ b/tests/e2e/tests/undoRedo.spec.ts
@@ -1,4 +1,4 @@
-import { test, expect } from "@playwright/test";
+import { test, expect, type Page } from "@playwright/test";
test.describe('undo and redo', () => {
const TYPE = 'button';
@@ -7,13 +7,17 @@ test.describe('undo and redo', () => {
const COLUMN2 = ".CoreColumns .CoreColumn:nth-child(2 of .CoreColumn)";
let url: string;
- test.beforeAll(async ({request}) => {
+ const closeSettingsBar = async (page: Page) => {
+ await page.locator('.BuilderSettings button[data-automation-action="close"]').click();
+ }
+
+ test.beforeAll(async ({ request }) => {
const response = await request.post(`/preset/2columns`);
expect(response.ok()).toBeTruthy();
- ({url} = await response.json());
+ ({ url } = await response.json());
});
- test.afterAll(async ({request}) => {
+ test.afterAll(async ({ request }) => {
await request.delete(url);
});
@@ -50,6 +54,7 @@ test.describe('undo and redo', () => {
await page
.locator('.BuilderFieldsText[data-automation-key="text"] input')
.fill('cool text');
+ await closeSettingsBar(page);
await page.locator("button.undo").click();
await expect(page.locator(COMPONENT_LOCATOR)).toHaveText('Button Text')
await page.locator("button.redo").click();