diff --git a/core/deployment/src/main/resources/dev-ui/components/qwc-flow-workflow-header.js b/core/deployment/src/main/resources/dev-ui/components/qwc-flow-workflow-header.js
new file mode 100644
index 000000000..d625d7aa8
--- /dev/null
+++ b/core/deployment/src/main/resources/dev-ui/components/qwc-flow-workflow-header.js
@@ -0,0 +1,75 @@
+import { LitElement, html, css } from 'lit';
+import '@vaadin/button';
+import '@vaadin/icon';
+
+/**
+* Shared header for workflow detail.
+* Renders a Back button plus the workflow name, namespace/version and description.
+*/
+export class QwcFlowWorkflowHeader extends LitElement {
+ static properties = {
+ // WorkflowDefinitionId from backend: { namespace, name, version }
+ workflowId: { type: Object },
+ description: { type: String },
+ };
+
+ static styles = css`
+ :host {
+ display: block;
+ }
+
+ .workflow-name {
+ margin: 12px 0px 4px 0px;
+ }
+
+ .workflow-meta {
+ font-size: var(--lumo-font-size-s);
+ color: var(--lumo-secondary-text-color);
+ margin-bottom: 12px;
+ }
+ `;
+
+ constructor() {
+ super();
+ this.workflowId = null;
+ this.description = '';
+ }
+
+ render() {
+ const id = this.workflowId || {};
+ const name = id.name || '(unknown)';
+ const ns = id.namespace || '';
+ const version = id.version || '';
+
+ return html`
+
+
+ Back
+
+
${name}
+
+ ${ns ? html`Namespace: ${ns}` : ''}
+ ${version ? html`${ns ? ' · ' : ''}Version: ${version}` : ''}
+ ${this.description
+ ? html`
+
+ ${this.description}
+ `
+ : ''}
+
+ `;
+ }
+
+ _backAction() {
+ this.dispatchEvent(
+ new CustomEvent('flow-header-back', {
+ detail: {},
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ })
+ );
+ }
+}
+
+customElements.define('qwc-flow-workflow-header', QwcFlowWorkflowHeader);
diff --git a/core/deployment/src/main/resources/dev-ui/qwc-flow-workflow-execution.js b/core/deployment/src/main/resources/dev-ui/qwc-flow-workflow-execution.js
index 4d5e63804..32429673c 100644
--- a/core/deployment/src/main/resources/dev-ui/qwc-flow-workflow-execution.js
+++ b/core/deployment/src/main/resources/dev-ui/qwc-flow-workflow-execution.js
@@ -16,6 +16,8 @@ import '@vaadin/number-field';
import '@vaadin/checkbox';
import '@vaadin/text-area';
+import './components/qwc-flow-workflow-header.js'
+
import 'qui-themed-code-block';
import 'qui-badge';
@@ -58,16 +60,6 @@ export class QwcFlowExecution extends observeState(QwcHotReloadElement) {
justify-content: flex-start;
}
- .workflow-name {
- margin: 12px 0px 4px 0px;
- }
-
- .workflow-meta {
- font-size: var(--lumo-font-size-s);
- color: var(--lumo-secondary-text-color);
- margin-bottom: 12px;
- }
-
.form-field {
margin-bottom: 8px;
max-width: 500px;
@@ -138,27 +130,12 @@ export class QwcFlowExecution extends observeState(QwcHotReloadElement) {
}
_renderTopBar() {
- const id = this.workflowId || {};
- const name = id.name || '(unknown)';
- const ns = id.namespace || '';
- const version = id.version || '';
-
return html`
-
-
-
- Back
-
-
${name}
-
- ${ns ? html`Namespace: ${ns}` : ''}
- ${version ? html`${ns ? ' · ' : ''}Version: ${version}` : ''}
- ${this.description ? html`
-
- ${this.description}
- ` : ''}
-
-
+
+
`;
}
diff --git a/core/deployment/src/main/resources/dev-ui/qwc-flow-workflows.js b/core/deployment/src/main/resources/dev-ui/qwc-flow-workflows.js
index 678188a79..666c990f5 100644
--- a/core/deployment/src/main/resources/dev-ui/qwc-flow-workflows.js
+++ b/core/deployment/src/main/resources/dev-ui/qwc-flow-workflows.js
@@ -1,100 +1,73 @@
import { QwcHotReloadElement, html, css } from 'qwc-hot-reload-element';
-import { observeState } from 'lit-element-state';
import { JsonRpc } from 'jsonrpc';
import '@vaadin/grid';
import '@vaadin/button';
import '@vaadin/icon';
-import '@vaadin/dialog';
-import { dialogRenderer } from '@vaadin/dialog/lit.js';
import { columnBodyRenderer } from '@vaadin/grid/lit.js';
import './components/openworkflowspec-diagram-editor.js';
+import './components/qwc-flow-workflow-header.js';
import './qwc-flow-workflow-execution.js';
-import { themeState } from 'theme-state';
-export class QwcFlow extends observeState(QwcHotReloadElement) {
+export class QwcFlow extends QwcHotReloadElement {
jsonRpc = new JsonRpc(this);
static styles = css`
- .workflows {
- padding: 8px;
- }
- `;
+ :host {
+ display: block;
+ height: 100%;
+ min-height: 0;
+ }
+
+ .workflows {
+ padding: 8px;
+ }
+
+ .diagram-view {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ min-height: 0;
+ padding: 0px 8px;
+ }
+
+ .diagram-view-header {
+ flex-shrink: 0;
+ }
+
+ /* min-height matches openworkflowspec-diagram-editor.js so the
+ diagram never collapses if an ancestor has no definite height. */
+ .diagram-view-body {
+ flex: 1 1 auto;
+ min-height: 520px;
+ position: relative;
+ overflow: hidden;
+ }
+
+ .diagram-editor {
+ height: 100%;
+ }
+ `;
static properties = {
_workflows: { state: true },
- _currentDiagramEditorWorkflow: { state: true },
- _diagramEditorDialogOpened: { state: true },
- _selectedWorkflow: { state: true }
+ _view: { state: true }
};
constructor() {
super();
this._workflows = [];
- this._currentDiagramEditorWorkflow = null;
- this._diagramEditorDialogOpened = false;
- this._selectedWorkflow = null;
+ this._view = { mode: 'list', workflow: null };
}
render() {
- if (this._selectedWorkflow != null) {
- // Pass WorkflowDefinitionId as a *property*, not as a string attribute
- return html`
-
-
- `;
- } else {
- return html`
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- html`
- this._visualizeDiagramEditor(workflow)}
- id="see-${this._generateDiagramEditorId(workflow.id)}">
-
-
- this._executeWorkflow(workflow)}>
-
-
- `, [])}>
-
-
- ${this._diagramEditorDialog()}
-
- `;
- }
+ switch(this._view.mode){
+ case 'diagram':
+ return this._renderDiagramView(this._view.workflow);
+ case 'execute':
+ return this._renderExecuteView(this._view.workflow)
+ default:
+ return this._renderWorkflowList();
+ }
}
hotReload() {
@@ -108,51 +81,105 @@ export class QwcFlow extends observeState(QwcHotReloadElement) {
});
}
+ _renderWorkflowList() {
+ return html`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ html`
+ this._viewDiagram(workflow)}
+ id="see-${this._generateDiagramEditorId(workflow.id)}"
+ title="View diagram">
+
+
+ this._executeWorkflow(workflow)}
+ title="Execute workflow">
+
+
+ `, [])}>
+
+
+
+ `;
+ }
+
+ _renderDiagramView(workflow) {
+ return html`
+
+ `;
+ }
+
+ _renderExecuteView(workflow) {
+ return html`
+
+
+ `;
+ }
+
_generateDiagramEditorId(workflowId) {
const { namespace, name, version } = workflowId;
return 'diagramEditor-' + `${namespace.replaceAll('.', '-')}-${name.replaceAll('.', '-')}-${version.replaceAll('.', '-')}`;
}
- _visualizeDiagramEditor(workflow) {
- this._diagramEditorDialogOpened = true;
- // Force new object to trigger re-render and not use stale workflow reference
- this._currentDiagramEditorWorkflow = {...workflow};
- }
-
- _diagramEditorDialog() {
- return html`
- (this._diagramEditorDialogOpened = e.detail.value)}
- ${dialogRenderer(() => this._diagramEditorContent(), [
- this._diagramEditorDialogOpened
- ])}
- .width=${'80%'}
- .height=${'80%'}
- resizable
- draggable
- header-title="Flow Diagram"
- theme="${themeState.theme.name}">
-
- `;
- }
-
- _diagramEditorContent() {
- return html`
-
-
- `;
+ _viewDiagram(workflow) {
+ this._view = { mode: 'diagram', workflow }
}
_executeWorkflow(workflow) {
- this._selectedWorkflow = workflow;
+ this._view = { mode: 'execute', workflow }
}
_showWorkflows() {
- this._selectedWorkflow = null;
+ this._view = { mode: 'list', workflow: null }
}
}
diff --git a/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorRoundTripTest.java b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorRoundTripTest.java
index a1c27c8cb..f29047a10 100644
--- a/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorRoundTripTest.java
+++ b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorRoundTripTest.java
@@ -276,13 +276,52 @@ void editorRendersWorkflow(
page.navigate(DEV_UI_URL);
- // Step 1 — wait for the grid data to arrive (any eye button in the DOM).
- // The vaadin-grid virtual-scroll only renders rows currently in the viewport;
- // rows outside the visible area are not attached to the DOM at all.
- // We use locator.evaluate() — Playwright resolves the vaadin-grid element
- // through shadow DOM and then calls scrollToIndex on it directly, which is
- // the only reliable way to reach into a shadow root from page.evaluate().
+ // Step 1 — scroll the target row into teh DOM and click the eye button.
+ openDiagramFor(diagramEditorId);
+
+ // Step 2 — the eye button navigates to a full page diagram screen.
+ // The shared header attaching is the signal the navigation happened
+ page.locator("qwc-flow-workflow-header")
+ .waitFor(new Locator.WaitForOptions()
+ .setState(WaitForSelectorState.ATTACHED));
+
+ // Step 3 — diagram container appears (loading completes)
+ page.locator("[data-testid='diagram-container']").waitFor();
+
+ // Step 4 — expected task node is rendered with correct text
+ Locator taskNode = page.locator("[data-testid='" + taskTestId + "']");
+ taskNode.waitFor();
+ assertThat(taskNode.textContent())
+ .as("task node '%s' must contain text '%s'", taskTestId, taskName)
+ .contains(taskName);
+
+ // Step 5 - navigate back and re-enter: re-renders correctly (no stale state)
+ page.locator(".backButton").click();
+ page.locator("[data-testid='diagram-container']")
+ .waitFor(new Locator.WaitForOptions()
+ .setState(WaitForSelectorState.DETACHED));
+
+ // Back on the list the grid re-virtualises, so scroll to row again
+ openDiagramFor(diagramEditorId);
+ page.locator("[data-testid='diagram-container']").waitFor();
+
+ Locator taskNodeAfterReopen = page.locator("[data-testid='" + taskTestId + "']");
+ taskNodeAfterReopen.waitFor();
+ assertThat(taskNodeAfterReopen.textContent())
+ .as("task node '%s' must still render correctly after reopen", taskTestId)
+ .contains(taskName);
+ }
+
+ // Scrolls the workflow grid until the row for {@code diagramEditorId} is rendered,
+ // then clicks its eye button.
+ // The vaadin-grid virtual-scroll only renders rows currently in the viewport;
+ // rows outside the visible area are not attached to the DOM at all. We use
+ // locator.evaluate() — Playwright resolves the vaadin-grid element through shadow
+ // DOM and then calls scrollToIndex on it directly, which is the only reliable way
+ // to reach into a shadow root from page.evaluate().
+ private void openDiagramFor(String diagramEditorId) {
String buttonSelector = "#see-" + diagramEditorId;
+ // Wait for the grid data to arrive (any eye button in the DOM).
page.waitForSelector("[id^='see-diagramEditor-']");
// Find the row index whose generated button id matches our target, then
// scroll the grid to that index so Vaadin renders the row into the DOM.
@@ -304,39 +343,6 @@ void editorRendersWorkflow(
new Page.WaitForSelectorOptions()
.setState(WaitForSelectorState.ATTACHED)
.setTimeout(10_000));
-
- // Step 2 — click eye button; wait for dialog to attach (it is inside a Vaadin
- // overlay and never becomes "visible" in Playwright's sense while animating)
page.locator(buttonSelector).click();
- page.locator("vaadin-dialog[opened]")
- .waitFor(new Locator.WaitForOptions()
- .setState(WaitForSelectorState.ATTACHED));
-
- // Step 3 — diagram container appears (loading completes)
- page.locator("[data-testid='diagram-container']").waitFor();
-
- // Step 4 — expected task node is rendered with correct text
- Locator taskNode = page.locator("[data-testid='" + taskTestId + "']");
- taskNode.waitFor();
- assertThat(taskNode.textContent())
- .as("task node '%s' must contain text '%s'", taskTestId, taskName)
- .contains(taskName);
-
- // Step 5 — close and reopen: re-renders correctly (no stale state)
- page.keyboard().press("Escape");
- page.locator("vaadin-dialog[opened]")
- .waitFor(new Locator.WaitForOptions()
- .setState(WaitForSelectorState.HIDDEN));
- page.locator(buttonSelector).click();
- page.locator("vaadin-dialog[opened]")
- .waitFor(new Locator.WaitForOptions()
- .setState(WaitForSelectorState.ATTACHED));
- page.locator("[data-testid='diagram-container']").waitFor();
-
- Locator taskNodeAfterReopen = page.locator("[data-testid='" + taskTestId + "']");
- taskNodeAfterReopen.waitFor();
- assertThat(taskNodeAfterReopen.textContent())
- .as("task node '%s' must still render correctly after reopen", taskTestId)
- .contains(taskName);
}
}
diff --git a/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorTest.java b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorTest.java
index 8410e443a..87cab3031 100644
--- a/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorTest.java
+++ b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorTest.java
@@ -82,7 +82,7 @@ void shouldRenderDiagramEditorWithSetEchoNode() {
page.waitForSelector(BUTTON_SELECTOR);
page.locator(BUTTON_SELECTOR).click();
- page.waitForSelector("vaadin-dialog[opened]",
+ page.waitForSelector("qwc-flow-workflow-header",
new Page.WaitForSelectorOptions().setState(WaitForSelectorState.ATTACHED));
page.waitForSelector("[data-testid='diagram-container']");