推进 INI 面板 control-page refresh workflow helper
This commit is contained in:
@@ -15,6 +15,8 @@ import {
|
||||
getIniPanelEntryByHref,
|
||||
getVisibleIniPanelEntries,
|
||||
createMachineSessionControlPageController,
|
||||
refreshMachineSessionControlPage,
|
||||
controlPageStatusText,
|
||||
renderMachineSessionControlView,
|
||||
} from "../runtime/ui/ini-panel/ui-shell.js";
|
||||
```
|
||||
@@ -45,7 +47,15 @@ and `label`.
|
||||
|
||||
Outer shells can call `createIniPanelShellViewModel()` when they need one
|
||||
read-only model with `pages`, `launcherLinks`, and `controlPage` helpers for
|
||||
mounting the existing control-page controller and renderer.
|
||||
mounting the existing control-page controller, refresh workflow, and renderer.
|
||||
|
||||
The control-page refresh workflow lives in
|
||||
`runtime/ui/ini-panel/control-page-refresh-workflow.js`. Use
|
||||
`refreshMachineSessionControlPage()` when an outer shell needs a structured
|
||||
refresh result with `phase`, `statusText`, `view`, `renderResult`, and `error`.
|
||||
`controlPageStatusText()` formats the already-observed panel status fields for
|
||||
the read-only status line, and `createControlPageRefreshState()` is the stable
|
||||
state-shape helper used by the workflow.
|
||||
|
||||
## Boundary
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
refreshMachineSessionControlPage,
|
||||
} from "./control-page-refresh-workflow.js";
|
||||
|
||||
export function createMachineSessionControlPageController({
|
||||
panelFrame,
|
||||
statusNode,
|
||||
@@ -34,19 +38,16 @@ export function createMachineSessionControlPageController({
|
||||
}
|
||||
|
||||
function render() {
|
||||
const view = getControlView();
|
||||
if (!view) {
|
||||
statusNode.textContent = "waiting for panel";
|
||||
viewNode.textContent = "";
|
||||
return null;
|
||||
}
|
||||
return refresh().renderResult;
|
||||
}
|
||||
|
||||
statusNode.textContent = [
|
||||
view.status.lastAction ?? "-",
|
||||
view.status.runStatus ?? "-",
|
||||
view.status.error ?? "-",
|
||||
].join(" | ");
|
||||
return renderControlView(viewNode, view);
|
||||
function refresh() {
|
||||
return refreshMachineSessionControlPage({
|
||||
getControlView,
|
||||
statusNode,
|
||||
viewNode,
|
||||
renderControlView,
|
||||
});
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
@@ -72,6 +73,7 @@ export function createMachineSessionControlPageController({
|
||||
getPanelApi,
|
||||
getPanelWindow,
|
||||
mount,
|
||||
refresh,
|
||||
render,
|
||||
startPolling,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
export function controlPageStatusText(status = {}) {
|
||||
return [
|
||||
status.lastAction ?? "-",
|
||||
status.runStatus ?? "-",
|
||||
status.error ?? "-",
|
||||
].join(" | ");
|
||||
}
|
||||
|
||||
export function createControlPageRefreshState({
|
||||
phase,
|
||||
statusText,
|
||||
view = null,
|
||||
renderResult = null,
|
||||
error = null,
|
||||
} = {}) {
|
||||
return {
|
||||
phase,
|
||||
statusText,
|
||||
view,
|
||||
renderResult,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function refreshMachineSessionControlPage({
|
||||
getControlView,
|
||||
statusNode,
|
||||
viewNode,
|
||||
renderControlView,
|
||||
} = {}) {
|
||||
if (typeof getControlView !== "function") {
|
||||
throw new Error("A control view getter is required.");
|
||||
}
|
||||
if (!statusNode) {
|
||||
throw new Error("A control page status node is required.");
|
||||
}
|
||||
if (!viewNode) {
|
||||
throw new Error("A control page view node is required.");
|
||||
}
|
||||
if (typeof renderControlView !== "function") {
|
||||
throw new Error("A control view renderer is required.");
|
||||
}
|
||||
|
||||
let view = null;
|
||||
try {
|
||||
view = getControlView() ?? null;
|
||||
} catch (error) {
|
||||
const statusText = `refresh failed | - | ${error.message}`;
|
||||
statusNode.textContent = statusText;
|
||||
viewNode.textContent = "";
|
||||
return createControlPageRefreshState({
|
||||
phase: "error",
|
||||
statusText,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (!view) {
|
||||
const statusText = "waiting for panel";
|
||||
statusNode.textContent = statusText;
|
||||
viewNode.textContent = "";
|
||||
return createControlPageRefreshState({
|
||||
phase: "waiting",
|
||||
statusText,
|
||||
});
|
||||
}
|
||||
|
||||
const statusText = controlPageStatusText(view.status);
|
||||
statusNode.textContent = statusText;
|
||||
try {
|
||||
const renderResult = renderControlView(viewNode, view);
|
||||
return createControlPageRefreshState({
|
||||
phase: "rendered",
|
||||
statusText,
|
||||
view,
|
||||
renderResult,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorStatusText = `refresh failed | - | ${error.message}`;
|
||||
statusNode.textContent = errorStatusText;
|
||||
viewNode.textContent = "";
|
||||
return createControlPageRefreshState({
|
||||
phase: "error",
|
||||
statusText: errorStatusText,
|
||||
view,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -21,5 +21,6 @@ controller.mount();
|
||||
window.linuxCncIniPanelControlPageApi = {
|
||||
getControlView: controller.getControlView,
|
||||
getPanelApi: controller.getPanelApi,
|
||||
refreshControlPage: controller.refresh,
|
||||
renderControlPage: controller.render,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,16 @@ import {
|
||||
import {
|
||||
renderMachineSessionControlView,
|
||||
} from "./control-view-renderer.js";
|
||||
import {
|
||||
controlPageStatusText,
|
||||
createControlPageRefreshState,
|
||||
refreshMachineSessionControlPage,
|
||||
} from "./control-page-refresh-workflow.js";
|
||||
export {
|
||||
controlPageStatusText,
|
||||
createControlPageRefreshState,
|
||||
refreshMachineSessionControlPage,
|
||||
} from "./control-page-refresh-workflow.js";
|
||||
export {
|
||||
INI_PANEL_ENTRIES,
|
||||
getIniPanelEntryByHref,
|
||||
@@ -43,6 +53,7 @@ export function createIniPanelShellViewModel(entries = getVisibleIniPanelEntries
|
||||
launcherLinks: createIniPanelLauncherLinkViewModel(entries),
|
||||
controlPage: {
|
||||
createController: createMachineSessionControlPageController,
|
||||
refresh: refreshMachineSessionControlPage,
|
||||
renderView: renderMachineSessionControlView,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@ TOOL_TABLE = browser-tool.tbl
|
||||
if (
|
||||
!controlPageApi?.getControlView ||
|
||||
!controlPageApi?.getPanelApi ||
|
||||
!controlPageApi?.refreshControlPage ||
|
||||
!controlPageApi?.renderControlPage
|
||||
) {
|
||||
throw new Error("control page API namespace did not expose control helpers");
|
||||
@@ -82,6 +83,11 @@ TOOL_TABLE = browser-tool.tbl
|
||||
() => controlPageApi.getControlView(),
|
||||
"control page API view",
|
||||
);
|
||||
assertEqual(
|
||||
controlPageApi.refreshControlPage().phase,
|
||||
"rendered",
|
||||
"control page refresh phase",
|
||||
);
|
||||
|
||||
panelDoc.getElementById("ini-editor").value = iniText;
|
||||
panelDoc.getElementById("query").click();
|
||||
|
||||
@@ -3,6 +3,9 @@ import assert from "node:assert/strict";
|
||||
import {
|
||||
createMachineSessionControlPageController,
|
||||
} from "../../../runtime/ui/ini-panel/control-page-controller.js";
|
||||
import {
|
||||
controlPageStatusText,
|
||||
} from "../../../runtime/ui/ini-panel/control-page-refresh-workflow.js";
|
||||
|
||||
function createPanelFrame(panelApi = null) {
|
||||
const listeners = new Map();
|
||||
@@ -75,6 +78,9 @@ const renderResult = controller.render();
|
||||
assert.deepEqual(renderResult, { statusNode: { textContent: "rendered" } });
|
||||
assert.equal(statusNode.textContent, "run-gcode | ok | -");
|
||||
assert.deepEqual(rendered, [{ container: viewNode, view: expectedView }]);
|
||||
const refreshResult = controller.refresh();
|
||||
assert.equal(refreshResult.phase, "rendered");
|
||||
assert.equal(refreshResult.statusText, controlPageStatusText(expectedView.status));
|
||||
|
||||
const mountedFrame = createPanelFrame(panelApi);
|
||||
const mountedStatusNode = { textContent: "" };
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
controlPageStatusText,
|
||||
createControlPageRefreshState,
|
||||
refreshMachineSessionControlPage,
|
||||
} from "../../../runtime/ui/ini-panel/control-page-refresh-workflow.js";
|
||||
|
||||
const expectedView = {
|
||||
status: {
|
||||
lastAction: "run-gcode",
|
||||
runStatus: "ok",
|
||||
error: null,
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
id: "run",
|
||||
rows: [
|
||||
{ label: "Canonical events", value: 2 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert.equal(controlPageStatusText(expectedView.status), "run-gcode | ok | -");
|
||||
assert.deepEqual(createControlPageRefreshState({ phase: "waiting", statusText: "waiting for panel" }), {
|
||||
phase: "waiting",
|
||||
statusText: "waiting for panel",
|
||||
view: null,
|
||||
renderResult: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const waitingStatusNode = { textContent: "" };
|
||||
const waitingViewNode = { textContent: "previous" };
|
||||
assert.deepEqual(
|
||||
refreshMachineSessionControlPage({
|
||||
getControlView: () => null,
|
||||
statusNode: waitingStatusNode,
|
||||
viewNode: waitingViewNode,
|
||||
renderControlView: () => "unused",
|
||||
}),
|
||||
{
|
||||
phase: "waiting",
|
||||
statusText: "waiting for panel",
|
||||
view: null,
|
||||
renderResult: null,
|
||||
error: null,
|
||||
},
|
||||
);
|
||||
assert.equal(waitingStatusNode.textContent, "waiting for panel");
|
||||
assert.equal(waitingViewNode.textContent, "");
|
||||
|
||||
const renderedStatusNode = { textContent: "" };
|
||||
const renderedViewNode = { textContent: "" };
|
||||
let renderedInput = null;
|
||||
assert.deepEqual(
|
||||
refreshMachineSessionControlPage({
|
||||
getControlView: () => expectedView,
|
||||
statusNode: renderedStatusNode,
|
||||
viewNode: renderedViewNode,
|
||||
renderControlView: (container, view) => {
|
||||
renderedInput = { container, view };
|
||||
return { rendered: true };
|
||||
},
|
||||
}),
|
||||
{
|
||||
phase: "rendered",
|
||||
statusText: "run-gcode | ok | -",
|
||||
view: expectedView,
|
||||
renderResult: { rendered: true },
|
||||
error: null,
|
||||
},
|
||||
);
|
||||
assert.deepEqual(renderedInput, { container: renderedViewNode, view: expectedView });
|
||||
assert.equal(renderedStatusNode.textContent, "run-gcode | ok | -");
|
||||
|
||||
const getterErrorStatusNode = { textContent: "" };
|
||||
const getterErrorViewNode = { textContent: "previous" };
|
||||
assert.deepEqual(
|
||||
refreshMachineSessionControlPage({
|
||||
getControlView: () => {
|
||||
throw new Error("panel unavailable");
|
||||
},
|
||||
statusNode: getterErrorStatusNode,
|
||||
viewNode: getterErrorViewNode,
|
||||
renderControlView: () => "unused",
|
||||
}),
|
||||
{
|
||||
phase: "error",
|
||||
statusText: "refresh failed | - | panel unavailable",
|
||||
view: null,
|
||||
renderResult: null,
|
||||
error: "panel unavailable",
|
||||
},
|
||||
);
|
||||
assert.equal(getterErrorViewNode.textContent, "");
|
||||
|
||||
const renderErrorStatusNode = { textContent: "" };
|
||||
const renderErrorViewNode = { textContent: "previous" };
|
||||
assert.deepEqual(
|
||||
refreshMachineSessionControlPage({
|
||||
getControlView: () => expectedView,
|
||||
statusNode: renderErrorStatusNode,
|
||||
viewNode: renderErrorViewNode,
|
||||
renderControlView: () => {
|
||||
throw new Error("renderer unavailable");
|
||||
},
|
||||
}),
|
||||
{
|
||||
phase: "error",
|
||||
statusText: "refresh failed | - | renderer unavailable",
|
||||
view: expectedView,
|
||||
renderResult: null,
|
||||
error: "renderer unavailable",
|
||||
},
|
||||
);
|
||||
assert.equal(renderErrorStatusNode.textContent, "refresh failed | - | renderer unavailable");
|
||||
assert.equal(renderErrorViewNode.textContent, "");
|
||||
|
||||
assert.throws(
|
||||
() => refreshMachineSessionControlPage(),
|
||||
/control view getter/,
|
||||
);
|
||||
|
||||
console.log("ini_panel_control_page_refresh_workflow_node_smoke=ok");
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
|
||||
node "$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_refresh_workflow.mjs"
|
||||
@@ -5,8 +5,10 @@ import { dirname, resolve } from "node:path";
|
||||
|
||||
import {
|
||||
INI_PANEL_ENTRIES,
|
||||
controlPageStatusText,
|
||||
createIniPanelLauncherLinkViewModel,
|
||||
createIniPanelShellViewModel,
|
||||
refreshMachineSessionControlPage,
|
||||
getVisibleIniPanelEntries,
|
||||
} from "../../../runtime/ui/ini-panel/ui-shell.js";
|
||||
|
||||
@@ -14,14 +16,20 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, "../../..");
|
||||
const docText = readFileSync(resolve(root, "docs/panel-entry.md"), "utf8");
|
||||
const shellText = readFileSync(resolve(root, "runtime/ui/ini-panel/ui-shell.js"), "utf8");
|
||||
const refreshWorkflowText = readFileSync(
|
||||
resolve(root, "runtime/ui/ini-panel/control-page-refresh-workflow.js"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const requiredShellExports = [
|
||||
"INI_PANEL_ENTRIES",
|
||||
"controlPageStatusText",
|
||||
"createIniPanelLauncherLinkViewModel",
|
||||
"createIniPanelShellViewModel",
|
||||
"getIniPanelEntryByHref",
|
||||
"getVisibleIniPanelEntries",
|
||||
"createMachineSessionControlPageController",
|
||||
"refreshMachineSessionControlPage",
|
||||
"renderMachineSessionControlView",
|
||||
];
|
||||
|
||||
@@ -30,6 +38,15 @@ for (const exportName of requiredShellExports) {
|
||||
assert.match(docText, new RegExp(`\\b${exportName}\\b`));
|
||||
}
|
||||
|
||||
for (const exportName of [
|
||||
"controlPageStatusText",
|
||||
"createControlPageRefreshState",
|
||||
"refreshMachineSessionControlPage",
|
||||
]) {
|
||||
assert.match(refreshWorkflowText, new RegExp(`\\bexport function ${exportName}\\b`));
|
||||
assert.match(docText, new RegExp(`\\b${exportName}\\b`));
|
||||
}
|
||||
|
||||
const requiredPages = [
|
||||
["Launch", "runtime/ui/ini-panel/launch.html"],
|
||||
["Edit and run", "runtime/ui/ini-panel/index.html"],
|
||||
@@ -51,7 +68,9 @@ assert.deepEqual(
|
||||
);
|
||||
assert.deepEqual(createIniPanelShellViewModel().pages, getVisibleIniPanelEntries());
|
||||
assert.deepEqual(createIniPanelShellViewModel().launcherLinks, createIniPanelLauncherLinkViewModel());
|
||||
assert.equal(controlPageStatusText({ lastAction: "run-gcode", runStatus: "ok", error: null }), "run-gcode | ok | -");
|
||||
assert.equal(typeof createIniPanelShellViewModel().controlPage.createController, "function");
|
||||
assert.equal(createIniPanelShellViewModel().controlPage.refresh, refreshMachineSessionControlPage);
|
||||
assert.equal(typeof createIniPanelShellViewModel().controlPage.renderView, "function");
|
||||
|
||||
for (const entry of INI_PANEL_ENTRIES) {
|
||||
|
||||
@@ -2,19 +2,31 @@ import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
INI_PANEL_ENTRIES,
|
||||
controlPageStatusText,
|
||||
createIniPanelLauncherLinkViewModel,
|
||||
createIniPanelShellViewModel,
|
||||
createControlPageRefreshState,
|
||||
createMachineSessionControlPageController,
|
||||
getIniPanelEntryByHref,
|
||||
getIniPanelEntryManifest,
|
||||
getVisibleIniPanelEntries,
|
||||
refreshMachineSessionControlPage,
|
||||
renderMachineSessionControlView,
|
||||
} from "../../../runtime/ui/ini-panel/ui-shell.js";
|
||||
|
||||
assert.deepEqual(getIniPanelEntryManifest(), INI_PANEL_ENTRIES);
|
||||
assert.deepEqual(getVisibleIniPanelEntries(), INI_PANEL_ENTRIES);
|
||||
assert.equal(controlPageStatusText({ lastAction: "x", runStatus: "ok", error: null }), "x | ok | -");
|
||||
assert.deepEqual(createControlPageRefreshState({ phase: "waiting", statusText: "waiting for panel" }), {
|
||||
phase: "waiting",
|
||||
statusText: "waiting for panel",
|
||||
view: null,
|
||||
renderResult: null,
|
||||
error: null,
|
||||
});
|
||||
assert.equal(getIniPanelEntryByHref("./index.html")?.id, "edit-run");
|
||||
assert.equal(typeof createMachineSessionControlPageController, "function");
|
||||
assert.equal(typeof refreshMachineSessionControlPage, "function");
|
||||
assert.equal(typeof renderMachineSessionControlView, "function");
|
||||
|
||||
const shellManifest = getIniPanelEntryManifest();
|
||||
@@ -53,6 +65,7 @@ assert.deepEqual(shellViewModel.pages, [
|
||||
]);
|
||||
assert.deepEqual(shellViewModel.launcherLinks, createIniPanelLauncherLinkViewModel());
|
||||
assert.equal(shellViewModel.controlPage.createController, createMachineSessionControlPageController);
|
||||
assert.equal(shellViewModel.controlPage.refresh, refreshMachineSessionControlPage);
|
||||
assert.equal(shellViewModel.controlPage.renderView, renderMachineSessionControlView);
|
||||
shellViewModel.pages[0].title = "changed";
|
||||
shellViewModel.launcherLinks[0].label = "changed";
|
||||
@@ -89,5 +102,6 @@ const controller = createMachineSessionControlPageController({
|
||||
assert.deepEqual(controller.render(), { rendered: true });
|
||||
assert.equal(statusNode.textContent, "run-gcode | ok | -");
|
||||
assert.equal(renderedView.status.runStatus, "ok");
|
||||
assert.equal(controller.refresh().phase, "rendered");
|
||||
|
||||
console.log("ini_panel_ui_shell_node_smoke=ok");
|
||||
|
||||
@@ -10,6 +10,7 @@ ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
"$ROOT_DIR/tests/ui/node/verify_ini_panel_machine_file_summary.sh"
|
||||
"$ROOT_DIR/tests/ui/node/verify_ini_panel_state_summary.sh"
|
||||
"$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_controller.sh"
|
||||
"$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_refresh_workflow.sh"
|
||||
"$ROOT_DIR/tests/ui/node/verify_ini_panel_entry_manifest.sh"
|
||||
"$ROOT_DIR/tests/ui/node/verify_ini_panel_ui_shell.sh"
|
||||
"$ROOT_DIR/tests/ui/node/verify_ini_panel_entry_docs.sh"
|
||||
|
||||
Reference in New Issue
Block a user