@nanobpm/urban 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/core/host.d.ts +31 -5
- package/dist/runtime/core/modules/pages.js +341 -20
- package/dist/runtime/engine/nanosdk.d.ts +6 -5
- package/dist/runtime/engine/nanosdk.js +6 -0
- package/dist/runtime/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/runtime/core/host.ts +32 -5
- package/src/runtime/core/modules/actions.test.ts +1 -0
- package/src/runtime/core/modules/agent.test.ts +1 -0
- package/src/runtime/core/modules/api.test.ts +1 -0
- package/src/runtime/core/modules/cancel.test.ts +1 -0
- package/src/runtime/core/modules/connectors.test.ts +1 -0
- package/src/runtime/core/modules/datasource.migrations.test.ts +1 -0
- package/src/runtime/core/modules/instance-tracking.test.ts +1 -0
- package/src/runtime/core/modules/pages.test.ts +128 -0
- package/src/runtime/core/modules/pages.ts +341 -20
- package/src/runtime/core/modules/surfaces.test.ts +2 -0
- package/src/runtime/core/modules/triggers.test.ts +1 -0
- package/src/runtime/core/modules/workers.test.ts +1 -0
- package/src/runtime/core/runtime.extensions.test.ts +3 -0
- package/src/runtime/core/runtime.test.ts +3 -0
- package/src/runtime/engine/nanosdk.test.ts +18 -0
- package/src/runtime/engine/nanosdk.ts +9 -4
- package/src/runtime/index.ts +1 -0
|
@@ -16,6 +16,7 @@ const noEngine: EngineClient = {
|
|
|
16
16
|
cancelInstance: async () => {},
|
|
17
17
|
publishMessage: async () => {},
|
|
18
18
|
searchUserTasks: async () => [],
|
|
19
|
+
openUserTasks: async () => [],
|
|
19
20
|
getForm: async () => null,
|
|
20
21
|
completeUserTask: async () => {},
|
|
21
22
|
searchProcessInstances: async () => [],
|
|
@@ -96,6 +96,7 @@ function fakeEngine(states: Record<string, ProcessInstanceSnapshot["state"]>): {
|
|
|
96
96
|
cancelInstance: notUsed("cancelInstance"),
|
|
97
97
|
publishMessage: notUsed("publishMessage"),
|
|
98
98
|
searchUserTasks: notUsed("searchUserTasks"),
|
|
99
|
+
openUserTasks: notUsed("openUserTasks"),
|
|
99
100
|
getForm: notUsed("getForm"),
|
|
100
101
|
completeUserTask: notUsed("completeUserTask"),
|
|
101
102
|
registerWorker: notUsed("registerWorker"),
|
|
@@ -40,6 +40,7 @@ function fakeEngine(): { engine: EngineClient; calls: FakeEngineCalls } {
|
|
|
40
40
|
calls.messages.push(input);
|
|
41
41
|
},
|
|
42
42
|
async searchUserTasks() { return []; },
|
|
43
|
+
async openUserTasks() { return []; },
|
|
43
44
|
async getForm() { return null; },
|
|
44
45
|
async completeUserTask() {},
|
|
45
46
|
async searchProcessInstances() { return []; },
|
|
@@ -891,6 +892,133 @@ test("the renderer ships a button node that opens a copy-pasteable modal", async
|
|
|
891
892
|
});
|
|
892
893
|
|
|
893
894
|
|
|
895
|
+
test("row-action confirm/error route through the in-DOM modal, never native confirm()/alert() (#276)", async () => {
|
|
896
|
+
// A sandboxed iframe without `allow-modals` makes window.confirm() return false
|
|
897
|
+
// WITHOUT prompting and turns alert()/prompt() into no-ops — so every
|
|
898
|
+
// confirm-guarded row action silently dead-ended (the !confirm(...) guard was
|
|
899
|
+
// always true → fireAction never ran) and its errors vanished. The fix routes
|
|
900
|
+
// both through urban's in-DOM openModal overlay, which works with no host
|
|
901
|
+
// sandbox change.
|
|
902
|
+
const res = await dispatch("GET", "/app/runtime.js");
|
|
903
|
+
const js = res.body ?? "";
|
|
904
|
+
// The confirm gate awaits an in-DOM dialog and only proceeds on a real choice.
|
|
905
|
+
assert.match(js, /if \(ra\.confirm && !\(await confirmModal\(ra\.confirm\)\)\) return/);
|
|
906
|
+
// Errors surface through the in-DOM alert instead of the suppressed native one.
|
|
907
|
+
assert.match(js, /alertModal\(String\(e\.message \|\| e\)\)/);
|
|
908
|
+
// The primitives exist and are built on the shared openModal overlay (no drift).
|
|
909
|
+
assert.match(js, /function confirmModal\(message\)/);
|
|
910
|
+
assert.match(js, /return new Promise\(\(resolve\) => \{/);
|
|
911
|
+
assert.match(js, /function alertModal\(message\)/);
|
|
912
|
+
// A dropped guarded open (a modal already showing) resolves false, never hangs.
|
|
913
|
+
assert.match(js, /if \(!opened\) resolve\(false\)/);
|
|
914
|
+
// openModal grew action buttons + a single-fire result callback so it can back a
|
|
915
|
+
// decision dialog; a dismissal (Escape/backdrop/✕/page switch) resolves the
|
|
916
|
+
// caller's default (dismissValue).
|
|
917
|
+
assert.match(js, /Array\.isArray\(m\.actions\)/);
|
|
918
|
+
assert.match(js, /resultValue = a\.value; close\(\);/);
|
|
919
|
+
// Match the semantic call site only — teardown wraps onResult in a try/catch so
|
|
920
|
+
// a throwing callback can't abort focus restoration, so don't pin the control flow.
|
|
921
|
+
assert.match(js, /onResult\(resultValue\)/);
|
|
922
|
+
// Defect-class guard: NO native confirm()/alert()/prompt() CALL (invoked with a
|
|
923
|
+
// string/identifier/template argument) survives anywhere in the served bundle —
|
|
924
|
+
// they are all false/no-ops under a sandboxed iframe. This also catches qualified
|
|
925
|
+
// calls (window.confirm(...) / globalThis.alert(...)): the lookbehind only excludes
|
|
926
|
+
// a preceding word char (so confirmModal(...) / alertModal(...) don't match), not a
|
|
927
|
+
// preceding ".", so a member call still trips the guard. The argument-start class
|
|
928
|
+
// covers string/template literals and identifier heads including $ and _
|
|
929
|
+
// (e.g. confirm(_msg), alert($msg), prompt(`...`)). Empty-paren mentions in
|
|
930
|
+
// comments (e.g. "native confirm()") are documentation, not calls, so are not matched.
|
|
931
|
+
assert.doesNotMatch(js, /(?<!\w)(?:confirm|alert|prompt)\(\s*["'`$_a-zA-Z]/);
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
test("the renderer ships a data-bound prose/markdown list node (#274)", async () => {
|
|
936
|
+
const res = await dispatch("GET", "/app/runtime.js");
|
|
937
|
+
const js = res.body ?? "";
|
|
938
|
+
// prose is a first-class node type alongside dataGrid.
|
|
939
|
+
assert.match(js, /prose: renderProse/);
|
|
940
|
+
assert.match(js, /function renderProse\(node\)/);
|
|
941
|
+
// It binds a datasource like dataGrid via the SHARED dataUrl builder (no drift):
|
|
942
|
+
// dataUrl is hoisted to module scope and used by both renderers.
|
|
943
|
+
assert.match(js, /function dataUrl\(source, tbl, filters, order\)/);
|
|
944
|
+
assert.match(js, /getJSON\(dataUrl\(data\.source, data\.table, activeFilter, data\.orderBy\)\)/);
|
|
945
|
+
// A param-scoped list with no route param renders nothing (no field=empty query),
|
|
946
|
+
// mirroring the grid's eqParam short-circuit.
|
|
947
|
+
assert.match(js, /paramScoped && PARAM === ""/);
|
|
948
|
+
// The active filter is Array.isArray-guarded (matching other malformed-schema
|
|
949
|
+
// guards) so a truthy non-array filter degrades to [] instead of throwing on .some.
|
|
950
|
+
assert.match(js, /const activeFilter = Array\.isArray\(data\.filter\) \? data\.filter : \[\]/);
|
|
951
|
+
// Per-item: a header template (interpTemplate, text-only) over one markdown body field.
|
|
952
|
+
assert.match(js, /el\("div", \{ class: "pc-prose-head" \}, interpTemplate\(headerTpl, row\)\)/);
|
|
953
|
+
assert.match(js, /const bodyField = p\.body/);
|
|
954
|
+
assert.match(js, /for \(const b of mdToNodes\(raw\)\) bodyEl\.append\(b\)/);
|
|
955
|
+
// The reading measure is clamped to a sane ch range (default ~66) and applied inline.
|
|
956
|
+
assert.match(js, /measure = Math\.max\(40, Math\.min\(100, Math\.round\(measure\)\)\)/);
|
|
957
|
+
assert.match(js, /"max-width:" \+ measure \+ "ch"/);
|
|
958
|
+
// Own-property gate on the body field (matching interpTemplate) so it can't pick
|
|
959
|
+
// up prototype cruft.
|
|
960
|
+
assert.match(js, /Object\.prototype\.hasOwnProperty\.call\(row, bodyField\)/);
|
|
961
|
+
// It polls + honours pc:refresh like dataGrid, and registers disposers so a page
|
|
962
|
+
// switch stops the poll / removes the listener.
|
|
963
|
+
assert.match(js, /document\.addEventListener\("pc:refresh", refresh\)/);
|
|
964
|
+
assert.match(js, /const timer = setInterval\(refresh, p\.refreshMs\)/);
|
|
965
|
+
assert.match(js, /disposers\.push\(\(\) => clearInterval\(timer\)\)/);
|
|
966
|
+
// It returns a pc-card with an <h2> title so makeCollapsible gives it
|
|
967
|
+
// collapsible/defaultCollapsed parity for free.
|
|
968
|
+
assert.match(js, /el\("h2", \{\}, p\.title\)/);
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
test("the prose renderer ships a safe, dependency-free markdown → DOM converter (#274)", async () => {
|
|
972
|
+
const res = await dispatch("GET", "/app/runtime.js");
|
|
973
|
+
const js = res.body ?? "";
|
|
974
|
+
// Block + inline parsers exist.
|
|
975
|
+
assert.match(js, /function mdToNodes\(src\)/);
|
|
976
|
+
assert.match(js, /function mdInline\(text\)/);
|
|
977
|
+
// NEVER innerHTML: the whole renderer builds DOM via el()/textContent only, so a
|
|
978
|
+
// record's markdown body can't become an HTML/script injection vector.
|
|
979
|
+
assert.doesNotMatch(js, /\.innerHTML\s*=/);
|
|
980
|
+
// Nodes are materialised as known elements with text set via createTextNode.
|
|
981
|
+
assert.match(js, /nodes\.push\(document\.createTextNode\(buf\)\)/);
|
|
982
|
+
// Links are scheme-sanitised: only http(s)/mailto become anchors; anything else
|
|
983
|
+
// degrades to plain label text (no javascript:/data: hrefs).
|
|
984
|
+
assert.match(js, /function mdSafeHref\(href\)/);
|
|
985
|
+
assert.match(js, /\^\(\?:https\?:\|mailto:\)/);
|
|
986
|
+
assert.match(js, /el\("a", \{ href: href, target: "_blank", rel: "noopener noreferrer" \}/);
|
|
987
|
+
// Emphasis, strong, inline code, headings, lists, quotes, fences are supported.
|
|
988
|
+
assert.match(js, /el\("strong", \{\}/);
|
|
989
|
+
assert.match(js, /el\("em", \{\}/);
|
|
990
|
+
assert.match(js, /el\("code", \{ class: "pc-md-code" \}/);
|
|
991
|
+
assert.match(js, /el\("pre", \{ class: "pc-md-pre" \}/);
|
|
992
|
+
assert.match(js, /el\("blockquote", \{ class: "pc-md-quote" \}/);
|
|
993
|
+
// Thematic breaks emit a themed <hr> (styled via .pc-md-hr), not a bare default rule.
|
|
994
|
+
assert.match(js, /el\("hr", \{ class: "pc-md-hr" \}\)/);
|
|
995
|
+
assert.match(js, /const MD_UL = \/\^\[-\*\+\]\\s\+\//);
|
|
996
|
+
assert.match(js, /const MD_OL = \/\^\\d\+\[\.\)\]\\s\+\//);
|
|
997
|
+
// Underscore emphasis is word-boundary guarded so snake_case identifiers survive.
|
|
998
|
+
assert.match(js, /function mdIsWordChar\(c\)/);
|
|
999
|
+
assert.match(js, /!mdIsWordChar\(s\[i - 1\]\)/);
|
|
1000
|
+
// The code backtick is built from a char code so this source stays inside the
|
|
1001
|
+
// String.raw template literal.
|
|
1002
|
+
assert.match(js, /const MD_BACKTICK = String\.fromCharCode\(96\)/);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
test("the shell styles the prose/markdown list at a comfortable reading measure (#274)", async () => {
|
|
1006
|
+
const res = await dispatch("GET", "/");
|
|
1007
|
+
const html = res.body ?? "";
|
|
1008
|
+
// The list stacks items; each body is clamped to ~66ch by default (the JS sets
|
|
1009
|
+
// the per-node measure inline, this is the no-JS fallback).
|
|
1010
|
+
assert.match(html, /\.pc-prose-list \{[^}]*flex-direction:column[^}]*\}/);
|
|
1011
|
+
assert.match(html, /\.pc-prose-body \{ max-width:66ch; \}/);
|
|
1012
|
+
// The item header is a small muted caption; markdown blocks reuse --nano-* tokens.
|
|
1013
|
+
assert.match(html, /\.pc-prose-head \{/);
|
|
1014
|
+
assert.match(html, /\.pc-md-pre \{[^}]*var\(--nano-inset\)[^}]*\}/);
|
|
1015
|
+
assert.match(html, /\.pc-md-quote \{[^}]*var\(--nano-edge-strong\)[^}]*\}/);
|
|
1016
|
+
// Thematic breaks are themed via --nano-* tokens, matching the other markdown blocks.
|
|
1017
|
+
assert.match(html, /\.pc-md-hr \{[^}]*var\(--nano-edge\)[^}]*\}/);
|
|
1018
|
+
assert.match(html, /\.pc-prose-body a \{ color:var\(--nano-accent-strong\); \}/);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
|
|
894
1022
|
test("GET /app/data/<source>/<table> returns rows", async () => {
|
|
895
1023
|
const res = await dispatch("GET", "/app/data/app/orders");
|
|
896
1024
|
assert.equal(res.status, 200);
|
|
@@ -627,6 +627,30 @@ table.pc-grid th { font-weight:600; color:var(--nano-text-muted); }
|
|
|
627
627
|
.pc-group-toggle { display:flex; align-items:center; gap:.5rem; width:100%; padding:.4rem .6rem; background:transparent; border:0; color:inherit; font:inherit; font-weight:600; cursor:pointer; text-align:left; }
|
|
628
628
|
.pc-group-toggle:hover { background:var(--nano-hover); }
|
|
629
629
|
.pc-group-title { font-size:.9rem; }
|
|
630
|
+
/* Data-bound prose/markdown list (renderProse, #274). Long-form narrative
|
|
631
|
+
records — plan-review findings, coordination notes, escalation bodies — read
|
|
632
|
+
as a stacked, full-width document at a comfortable measure (~66ch) instead of
|
|
633
|
+
being jammed into a grid column. Each item stacks a small header over a
|
|
634
|
+
markdown-rendered body; the body max-width is set inline per node (props.measure)
|
|
635
|
+
with this rule as the no-JS fallback. */
|
|
636
|
+
.pc-prose-list { display:flex; flex-direction:column; gap:1.1rem; }
|
|
637
|
+
.pc-prose-item { border-top:1px solid var(--nano-edge); padding-top:.9rem; }
|
|
638
|
+
.pc-prose-item:first-child { border-top:0; padding-top:0; }
|
|
639
|
+
.pc-prose-head { font-size:.8rem; font-weight:600; color:var(--nano-text-muted); text-transform:uppercase; letter-spacing:.03em; margin:0 0 .35rem; }
|
|
640
|
+
.pc-prose-empty { color:var(--nano-text-faint); }
|
|
641
|
+
.pc-prose-body { max-width:66ch; }
|
|
642
|
+
.pc-prose-body > :first-child { margin-top:0; }
|
|
643
|
+
.pc-prose-body > :last-child { margin-bottom:0; }
|
|
644
|
+
.pc-md-p { margin:.55rem 0; overflow-wrap:anywhere; }
|
|
645
|
+
.pc-md-h { font-size:1.05rem; font-weight:650; margin:.95rem 0 .4rem; }
|
|
646
|
+
.pc-md-list { margin:.45rem 0; padding-left:1.35rem; }
|
|
647
|
+
.pc-md-list li { margin:.15rem 0; }
|
|
648
|
+
.pc-md-quote { margin:.6rem 0; padding:.1rem .9rem; border-left:3px solid var(--nano-edge-strong); color:var(--nano-text-muted); }
|
|
649
|
+
.pc-md-pre { background:var(--nano-inset); border:1px solid var(--nano-edge); border-radius:.4rem; padding:.7rem .8rem; overflow:auto; margin:.6rem 0; }
|
|
650
|
+
.pc-md-pre code { font:.82rem/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--nano-text); white-space:pre; }
|
|
651
|
+
.pc-md-code { background:var(--nano-inset); border-radius:.3rem; padding:.05rem .3rem; font:.85em ui-monospace,SFMono-Regular,Menlo,monospace; }
|
|
652
|
+
.pc-md-hr { border:0; border-top:1px solid var(--nano-edge); margin:1rem 0; }
|
|
653
|
+
.pc-prose-body a { color:var(--nano-accent-strong); }
|
|
630
654
|
/* A standalone button node + the modal it opens (e.g. a copy-pasteable prompt). */
|
|
631
655
|
.pc-buttonrow { margin:1rem 0; }
|
|
632
656
|
.pc-btn-ghost { background:transparent; color:var(--nano-text-muted); border:1px solid var(--nano-edge); }
|
|
@@ -735,8 +759,8 @@ table.pc-grid th { font-weight:600; color:var(--nano-text-muted); }
|
|
|
735
759
|
|
|
736
760
|
// The schema-driven browser renderer (ADR 0042 §3). Plain ES module string served at
|
|
737
761
|
// /app/runtime.js — it does NOT ship Craft.js (authoring is console-side only). It
|
|
738
|
-
// fetches the home page's page.json and renders text / actionForm / dataGrid
|
|
739
|
-
// wiring actionForm → /app/actions/start and dataGrid → /app/data (with a refresh).
|
|
762
|
+
// fetches the home page's page.json and renders text / actionForm / dataGrid / prose
|
|
763
|
+
// nodes, wiring actionForm → /app/actions/start and dataGrid/prose → /app/data (with a refresh).
|
|
740
764
|
const RENDERER_JS = String.raw`
|
|
741
765
|
const root = document.getElementById("page");
|
|
742
766
|
const HOME = root.dataset.home || "home";
|
|
@@ -1044,7 +1068,9 @@ function resolveCopyText(text) {
|
|
|
1044
1068
|
function openModal(m) {
|
|
1045
1069
|
// Only one modal at a time — drop a second (e.g. double-click) open so we
|
|
1046
1070
|
// never stack overlays or register duplicate document-level keydown handlers.
|
|
1047
|
-
|
|
1071
|
+
// Return null (not a close fn) so a guarded caller (confirmModal) can tell the
|
|
1072
|
+
// open was dropped and resolve rather than hang.
|
|
1073
|
+
if (modalOpen) return null;
|
|
1048
1074
|
modalOpen = true;
|
|
1049
1075
|
const overlay = el("div", { class: "pc-modal-overlay" });
|
|
1050
1076
|
// role="dialog" + aria-modal need an accessible name: label by the visible
|
|
@@ -1063,6 +1089,13 @@ function openModal(m) {
|
|
|
1063
1089
|
// Remember what had focus so we can restore it on close — a keyboard/screen
|
|
1064
1090
|
// reader user returns to where they were instead of being dumped at <body>.
|
|
1065
1091
|
const prevFocus = document.activeElement;
|
|
1092
|
+
// A dismissal (✕/Close button, backdrop, Escape, page switch) resolves with
|
|
1093
|
+
// m.dismissValue; an action button overrides it before closing. onResult fires
|
|
1094
|
+
// exactly once, from close(), so every exit path (button OR dismiss) reports a
|
|
1095
|
+
// result — this is what lets confirmModal() gate on a real user choice inside a
|
|
1096
|
+
// sandboxed iframe where native confirm() would silently return false (#276).
|
|
1097
|
+
const onResult = typeof m.onResult === "function" ? m.onResult : null;
|
|
1098
|
+
let resultValue = m.dismissValue;
|
|
1066
1099
|
let closed = false;
|
|
1067
1100
|
function close() {
|
|
1068
1101
|
if (closed) return;
|
|
@@ -1075,6 +1108,9 @@ function openModal(m) {
|
|
|
1075
1108
|
// accrete dead close() closures until the next page navigation runs teardown.
|
|
1076
1109
|
const i = disposers.indexOf(close);
|
|
1077
1110
|
if (i !== -1) disposers.splice(i, 1);
|
|
1111
|
+
// A consumer-provided result callback must never abort teardown: if it throws,
|
|
1112
|
+
// swallow it so focus restoration below (and Escape/backdrop close) still runs.
|
|
1113
|
+
if (onResult) { try { onResult(resultValue); } catch (_e) {} }
|
|
1078
1114
|
if (prevFocus && typeof prevFocus.focus === "function") prevFocus.focus();
|
|
1079
1115
|
}
|
|
1080
1116
|
// Focusable descendants of the dialog, in DOM order, for the Tab focus trap.
|
|
@@ -1107,7 +1143,29 @@ function openModal(m) {
|
|
|
1107
1143
|
}
|
|
1108
1144
|
const closeBtn = el("button", { class: "pc-btn pc-btn-sm pc-btn-ghost", type: "button" }, m.closeLabel || "Close");
|
|
1109
1145
|
closeBtn.addEventListener("click", close);
|
|
1110
|
-
actions
|
|
1146
|
+
// Custom action buttons (m.actions) turn this into a decision dialog — each
|
|
1147
|
+
// sets the result then closes; the plain Close button is the default when none
|
|
1148
|
+
// are given. This is the in-DOM replacement for native confirm()/alert(): one
|
|
1149
|
+
// overlay + focus-trap + teardown machinery, no drift (AGENTS.md).
|
|
1150
|
+
let focusBtn = closeBtn;
|
|
1151
|
+
if (Array.isArray(m.actions) && m.actions.length) {
|
|
1152
|
+
let firstActionBtn = null;
|
|
1153
|
+
let defaultBtn = null;
|
|
1154
|
+
for (const a of m.actions) {
|
|
1155
|
+
const cls = "pc-btn pc-btn-sm" + (a.variant === "ghost" ? " pc-btn-ghost" : "");
|
|
1156
|
+
const ab = el("button", { class: cls, type: "button" }, a.label);
|
|
1157
|
+
ab.addEventListener("click", () => { resultValue = a.value; close(); });
|
|
1158
|
+
actions.append(ab);
|
|
1159
|
+
if (!firstActionBtn) firstActionBtn = ab;
|
|
1160
|
+
if (a.default) defaultBtn = ab;
|
|
1161
|
+
}
|
|
1162
|
+
// Default focus lands on the FIRST action (typically Cancel), never the last:
|
|
1163
|
+
// for confirmModal that keeps a stray Enter from accidentally triggering the
|
|
1164
|
+
// destructive Confirm. An action can opt into autofocus with default:true.
|
|
1165
|
+
focusBtn = defaultBtn || firstActionBtn;
|
|
1166
|
+
} else {
|
|
1167
|
+
actions.append(closeBtn);
|
|
1168
|
+
}
|
|
1111
1169
|
dialog.append(actions);
|
|
1112
1170
|
overlay.append(dialog);
|
|
1113
1171
|
// A click on the backdrop (never the dialog itself) dismisses.
|
|
@@ -1117,7 +1175,36 @@ function openModal(m) {
|
|
|
1117
1175
|
// A page switch runs teardown() before the next render — dispose the modal
|
|
1118
1176
|
// there too so navigating away never leaves a stale overlay + keydown listener.
|
|
1119
1177
|
disposers.push(close);
|
|
1120
|
-
|
|
1178
|
+
focusBtn.focus();
|
|
1179
|
+
return close;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// In-DOM confirm — an accessible replacement for native confirm() that works in
|
|
1183
|
+
// a sandboxed iframe (without allow-modals native confirm() silently returns
|
|
1184
|
+
// false, dead-ending every confirm-guarded action; #276). Renders the message
|
|
1185
|
+
// with Cancel/Confirm and resolves the user's choice; a dismissal (Escape /
|
|
1186
|
+
// backdrop / ✕) resolves false. If a modal is already open the guarded open is
|
|
1187
|
+
// dropped — resolve false so the caller doesn't hang.
|
|
1188
|
+
function confirmModal(message) {
|
|
1189
|
+
return new Promise((resolve) => {
|
|
1190
|
+
const opened = openModal({
|
|
1191
|
+
title: "Confirm",
|
|
1192
|
+
description: String(message),
|
|
1193
|
+
dismissValue: false,
|
|
1194
|
+
actions: [
|
|
1195
|
+
{ label: "Cancel", value: false, variant: "ghost" },
|
|
1196
|
+
{ label: "Confirm", value: true },
|
|
1197
|
+
],
|
|
1198
|
+
onResult: (v) => resolve(!!v),
|
|
1199
|
+
});
|
|
1200
|
+
if (!opened) resolve(false);
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// In-DOM error surface — replaces native alert(), which a sandboxed iframe also
|
|
1205
|
+
// suppresses, so action failures reported through it were invisible (#276).
|
|
1206
|
+
function alertModal(message) {
|
|
1207
|
+
openModal({ title: "Error", description: String(message) });
|
|
1121
1208
|
}
|
|
1122
1209
|
|
|
1123
1210
|
// A standalone button node. Clicking it opens props.modal — used e.g. to surface
|
|
@@ -1735,6 +1822,248 @@ function renderActionForm(node) {
|
|
|
1735
1822
|
// Single source of truth for both the initial render and every refresh below.
|
|
1736
1823
|
function rowCountLabel(n) { return n + (n === 1 ? " row" : " rows"); }
|
|
1737
1824
|
|
|
1825
|
+
// Build the /app/data/<source>/<table> URL a data-bound list renderer fetches,
|
|
1826
|
+
// threading the whitelisted where/order query params. Shared by dataGrid and the
|
|
1827
|
+
// prose renderer so the two can't drift on how a filter/orderBy is encoded. A
|
|
1828
|
+
// filter is { field, in:[…] } (IN set), { field, eqParam:true } (bound to the
|
|
1829
|
+
// current route PARAM — "show the selected entity's rows"), or { field, eq } (a
|
|
1830
|
+
// literal equals); orderBy is { field, dir }. The source/table/field names are
|
|
1831
|
+
// server-side whitelisted (the /app/data route rejects unknown columns).
|
|
1832
|
+
function dataUrl(source, tbl, filters, order) {
|
|
1833
|
+
let u = "/app/data/" + encodeURIComponent(source) + "/" + encodeURIComponent(tbl);
|
|
1834
|
+
const qs = [];
|
|
1835
|
+
for (const f of filters || []) {
|
|
1836
|
+
if (Array.isArray(f.in)) qs.push("where=" + encodeURIComponent(f.field + ":in:" + f.in.join(",")));
|
|
1837
|
+
else if (f.eqParam) qs.push("where=" + encodeURIComponent(f.field + ":" + PARAM));
|
|
1838
|
+
else qs.push("where=" + encodeURIComponent(f.field + ":" + f.eq));
|
|
1839
|
+
}
|
|
1840
|
+
if (order && order.field) qs.push("order=" + encodeURIComponent(order.field + ":" + (order.dir || "asc")));
|
|
1841
|
+
return qs.length ? u + "?" + qs.join("&") : u;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// ── Minimal, safe Markdown → DOM (no innerHTML) — used by the prose renderer ──
|
|
1845
|
+
// A dependency-free CommonMark subset. It NEVER sets innerHTML: every block and
|
|
1846
|
+
// inline token is materialised as a known element via el(), with text set through
|
|
1847
|
+
// textContent / createTextNode — so a record's body field may carry markdown
|
|
1848
|
+
// WITHOUT ever becoming an HTML/script injection vector (the same text-only
|
|
1849
|
+
// guarantee interpTemplate gives a grid cell). Supported: ATX headings
|
|
1850
|
+
// (#..######), fenced + inline code, bold (**/__), italic (*/_), links
|
|
1851
|
+
// [text](url) (http/https/mailto only — every other scheme degrades to its plain
|
|
1852
|
+
// label), unordered (-,*,+) and ordered (1. / 1)) lists, blockquotes (>),
|
|
1853
|
+
// thematic breaks (---), and blank-line-separated paragraphs. The backtick used
|
|
1854
|
+
// for code spans/fences is built from a char code so this source stays inside the
|
|
1855
|
+
// String.raw template literal (which is itself delimited by a backtick).
|
|
1856
|
+
const MD_BACKTICK = String.fromCharCode(96);
|
|
1857
|
+
function mdIsWordChar(c) { return typeof c === "string" && /[0-9A-Za-z]/.test(c); }
|
|
1858
|
+
// Only http(s)/mailto hrefs become anchors; anything else (javascript:, data:,
|
|
1859
|
+
// vbscript:, a bare relative path, …) returns "" so the caller renders the label
|
|
1860
|
+
// as plain text rather than a clickable, potentially-hostile link.
|
|
1861
|
+
function mdSafeHref(href) {
|
|
1862
|
+
const h = String(href == null ? "" : href).trim();
|
|
1863
|
+
return /^(?:https?:|mailto:)/i.test(h) ? h : "";
|
|
1864
|
+
}
|
|
1865
|
+
// Find the closing run of 'marker' at or after 'from'. For underscore emphasis the
|
|
1866
|
+
// close must not be intra-word (a following alphanumeric means snake_case, not a
|
|
1867
|
+
// delimiter), so keep scanning past such runs; -1 when there is no valid close.
|
|
1868
|
+
function mdFindClose(s, marker, from, underscore) {
|
|
1869
|
+
let j = from;
|
|
1870
|
+
while (true) {
|
|
1871
|
+
const k = s.indexOf(marker, j);
|
|
1872
|
+
if (k < 0) return -1;
|
|
1873
|
+
if (!underscore || !mdIsWordChar(s[k + marker.length])) return k;
|
|
1874
|
+
j = k + marker.length;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
function mdInline(text) {
|
|
1878
|
+
const s = String(text == null ? "" : text);
|
|
1879
|
+
const nodes = [];
|
|
1880
|
+
let buf = "";
|
|
1881
|
+
const flush = () => { if (buf) { nodes.push(document.createTextNode(buf)); buf = ""; } };
|
|
1882
|
+
let i = 0;
|
|
1883
|
+
while (i < s.length) {
|
|
1884
|
+
const ch = s[i];
|
|
1885
|
+
// Backslash escape: the next char is emitted literally, never as a delimiter.
|
|
1886
|
+
if (ch === "\\" && i + 1 < s.length) { buf += s[i + 1]; i += 2; continue; }
|
|
1887
|
+
// Inline code span — verbatim, no nested inline parsing.
|
|
1888
|
+
if (ch === MD_BACKTICK) {
|
|
1889
|
+
const end = s.indexOf(MD_BACKTICK, i + 1);
|
|
1890
|
+
if (end > i) { flush(); nodes.push(el("code", { class: "pc-md-code" }, s.slice(i + 1, end))); i = end + 1; continue; }
|
|
1891
|
+
}
|
|
1892
|
+
// Strong (** or __). Underscore only when it opens on a word boundary so an
|
|
1893
|
+
// identifier like a__b doesn't get mangled into emphasis.
|
|
1894
|
+
if ((ch === "*" || ch === "_") && s[i + 1] === ch) {
|
|
1895
|
+
const underscore = ch === "_";
|
|
1896
|
+
if (!underscore || !mdIsWordChar(s[i - 1])) {
|
|
1897
|
+
const marker = ch + ch;
|
|
1898
|
+
const end = mdFindClose(s, marker, i + 2, underscore);
|
|
1899
|
+
if (end > i + 1) { flush(); nodes.push(el("strong", {}, ...mdInline(s.slice(i + 2, end)))); i = end + 2; continue; }
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
// Emphasis (* or _), same word-boundary guard for underscore.
|
|
1903
|
+
if (ch === "*" || ch === "_") {
|
|
1904
|
+
const underscore = ch === "_";
|
|
1905
|
+
if (!underscore || !mdIsWordChar(s[i - 1])) {
|
|
1906
|
+
const end = mdFindClose(s, ch, i + 1, underscore);
|
|
1907
|
+
if (end > i + 1) { flush(); nodes.push(el("em", {}, ...mdInline(s.slice(i + 1, end)))); i = end + 1; continue; }
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
// Link [label](href): href sanitised; an unsafe scheme drops to the label text.
|
|
1911
|
+
if (ch === "[") {
|
|
1912
|
+
const close = s.indexOf("]", i + 1);
|
|
1913
|
+
if (close > i && s[close + 1] === "(") {
|
|
1914
|
+
const paren = s.indexOf(")", close + 2);
|
|
1915
|
+
if (paren > close) {
|
|
1916
|
+
const href = mdSafeHref(s.slice(close + 2, paren));
|
|
1917
|
+
const label = mdInline(s.slice(i + 1, close));
|
|
1918
|
+
flush();
|
|
1919
|
+
if (href) nodes.push(el("a", { href: href, target: "_blank", rel: "noopener noreferrer" }, ...label));
|
|
1920
|
+
else for (const l of label) nodes.push(l);
|
|
1921
|
+
i = paren + 1; continue;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
buf += ch; i++;
|
|
1926
|
+
}
|
|
1927
|
+
flush();
|
|
1928
|
+
return nodes;
|
|
1929
|
+
}
|
|
1930
|
+
// Regexes that classify a block-opening line; shared by the block loop and the
|
|
1931
|
+
// paragraph terminator so the two can't disagree on where a paragraph ends.
|
|
1932
|
+
const MD_HEADING = /^(#{1,6})\s+(.*)$/;
|
|
1933
|
+
const MD_QUOTE = /^>\s?/;
|
|
1934
|
+
const MD_UL = /^[-*+]\s+/;
|
|
1935
|
+
const MD_OL = /^\d+[.)]\s+/;
|
|
1936
|
+
const MD_HR = /^([-*_])(?:\s*\1){2,}\s*$/;
|
|
1937
|
+
function mdBlockStart(t) {
|
|
1938
|
+
return t.slice(0, 3) === MD_BACKTICK + MD_BACKTICK + MD_BACKTICK
|
|
1939
|
+
|| MD_HEADING.test(t) || MD_QUOTE.test(t) || MD_UL.test(t) || MD_OL.test(t) || MD_HR.test(t);
|
|
1940
|
+
}
|
|
1941
|
+
// Parse a markdown string into an array of block-level DOM nodes.
|
|
1942
|
+
function mdToNodes(src) {
|
|
1943
|
+
const out = [];
|
|
1944
|
+
const fence = MD_BACKTICK + MD_BACKTICK + MD_BACKTICK;
|
|
1945
|
+
const lines = String(src == null ? "" : src).replace(/\r\n?/g, "\n").split("\n");
|
|
1946
|
+
let i = 0;
|
|
1947
|
+
while (i < lines.length) {
|
|
1948
|
+
const t = lines[i].trim();
|
|
1949
|
+
if (t === "") { i++; continue; }
|
|
1950
|
+
// Fenced code block — content is verbatim (no inline parsing).
|
|
1951
|
+
if (t.slice(0, 3) === fence) {
|
|
1952
|
+
const body = [];
|
|
1953
|
+
i++;
|
|
1954
|
+
while (i < lines.length && lines[i].trim().slice(0, 3) !== fence) { body.push(lines[i]); i++; }
|
|
1955
|
+
if (i < lines.length) i++; // consume the closing fence
|
|
1956
|
+
out.push(el("pre", { class: "pc-md-pre" }, el("code", {}, body.join("\n"))));
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
// Thematic break (checked before lists so "***"/"---" isn't read as a bullet).
|
|
1960
|
+
if (MD_HR.test(t)) { out.push(el("hr", { class: "pc-md-hr" })); i++; continue; }
|
|
1961
|
+
// ATX heading.
|
|
1962
|
+
const h = MD_HEADING.exec(t);
|
|
1963
|
+
if (h) { out.push(el("h" + h[1].length, { class: "pc-md-h" }, ...mdInline(h[2].trim()))); i++; continue; }
|
|
1964
|
+
// Blockquote — strip one '>' per line and re-parse the inner block(s).
|
|
1965
|
+
if (MD_QUOTE.test(t)) {
|
|
1966
|
+
const inner = [];
|
|
1967
|
+
while (i < lines.length && MD_QUOTE.test(lines[i].trim())) { inner.push(lines[i].trim().replace(MD_QUOTE, "")); i++; }
|
|
1968
|
+
const bq = el("blockquote", { class: "pc-md-quote" });
|
|
1969
|
+
for (const b of mdToNodes(inner.join("\n"))) bq.append(b);
|
|
1970
|
+
out.push(bq); continue;
|
|
1971
|
+
}
|
|
1972
|
+
// Unordered list.
|
|
1973
|
+
if (MD_UL.test(t)) {
|
|
1974
|
+
const ul = el("ul", { class: "pc-md-list" });
|
|
1975
|
+
while (i < lines.length && MD_UL.test(lines[i].trim())) {
|
|
1976
|
+
ul.append(el("li", {}, ...mdInline(lines[i].trim().replace(MD_UL, ""))));
|
|
1977
|
+
i++;
|
|
1978
|
+
}
|
|
1979
|
+
out.push(ul); continue;
|
|
1980
|
+
}
|
|
1981
|
+
// Ordered list.
|
|
1982
|
+
if (MD_OL.test(t)) {
|
|
1983
|
+
const ol = el("ol", { class: "pc-md-list" });
|
|
1984
|
+
while (i < lines.length && MD_OL.test(lines[i].trim())) {
|
|
1985
|
+
ol.append(el("li", {}, ...mdInline(lines[i].trim().replace(MD_OL, ""))));
|
|
1986
|
+
i++;
|
|
1987
|
+
}
|
|
1988
|
+
out.push(ol); continue;
|
|
1989
|
+
}
|
|
1990
|
+
// Paragraph — soft-wrapped lines joined with a space until a blank line or the
|
|
1991
|
+
// next block opener.
|
|
1992
|
+
const para = [];
|
|
1993
|
+
while (i < lines.length) {
|
|
1994
|
+
const lt = lines[i].trim();
|
|
1995
|
+
if (lt === "" || mdBlockStart(lt)) break;
|
|
1996
|
+
para.push(lt); i++;
|
|
1997
|
+
}
|
|
1998
|
+
out.push(el("p", { class: "pc-md-p" }, ...mdInline(para.join(" "))));
|
|
1999
|
+
}
|
|
2000
|
+
return out;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// A data-bound prose/markdown list (#274). Binds a datasource like dataGrid
|
|
2004
|
+
// (props.data = { source, table, orderBy, filter }) but renders each row as a
|
|
2005
|
+
// stacked prose block — a small header template (props.header, e.g.
|
|
2006
|
+
// "Round {{round}} · {{approved}}") over one body field (props.body) rendered as
|
|
2007
|
+
// sanitised markdown at a comfortable measure (props.measure ch, ~66 default).
|
|
2008
|
+
// This is the primitive for narrative records (plan-review findings, coordination
|
|
2009
|
+
// notes, escalation bodies) that read as a document, not a squeezed table cell.
|
|
2010
|
+
// It inherits collapsible/defaultCollapsed for free via makeCollapsible (which
|
|
2011
|
+
// unwraps the <h2> title), and refreshMs polling + pc:refresh like dataGrid.
|
|
2012
|
+
function renderProse(node) {
|
|
2013
|
+
const p = node.props || {};
|
|
2014
|
+
const data = p.data || {};
|
|
2015
|
+
const card = el("section", { class: "pc-card" });
|
|
2016
|
+
if (p.title) card.append(el("h2", {}, p.title));
|
|
2017
|
+
const listEl = el("div", { class: "pc-prose-list" });
|
|
2018
|
+
card.append(listEl);
|
|
2019
|
+
const headerTpl = typeof p.header === "string" ? p.header : null;
|
|
2020
|
+
const bodyField = p.body;
|
|
2021
|
+
// Clamp the reading measure to a sane range so a malformed schema can't set a
|
|
2022
|
+
// 5000ch (full-bleed) or 2ch (one-word-per-line) body. ~66ch is the default.
|
|
2023
|
+
let measure = Number(p.measure);
|
|
2024
|
+
if (!Number.isFinite(measure)) measure = 66;
|
|
2025
|
+
measure = Math.max(40, Math.min(100, Math.round(measure)));
|
|
2026
|
+
const activeFilter = Array.isArray(data.filter) ? data.filter : [];
|
|
2027
|
+
|
|
2028
|
+
function itemFor(row) {
|
|
2029
|
+
const item = el("article", { class: "pc-prose-item" });
|
|
2030
|
+
if (headerTpl) item.append(el("div", { class: "pc-prose-head" }, interpTemplate(headerTpl, row)));
|
|
2031
|
+
const bodyEl = el("div", { class: "pc-prose-body", style: "max-width:" + measure + "ch" });
|
|
2032
|
+
// Own-property gate (matching interpTemplate) so a body field name that
|
|
2033
|
+
// collides with a prototype key can't pick up prototype cruft.
|
|
2034
|
+
const raw = bodyField != null && Object.prototype.hasOwnProperty.call(row, bodyField) && row[bodyField] != null
|
|
2035
|
+
? String(row[bodyField])
|
|
2036
|
+
: "";
|
|
2037
|
+
for (const b of mdToNodes(raw)) bodyEl.append(b);
|
|
2038
|
+
item.append(bodyEl);
|
|
2039
|
+
return item;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
async function refresh() {
|
|
2043
|
+
try {
|
|
2044
|
+
// A param-scoped list ("show the selected entity's records") with no route
|
|
2045
|
+
// param present renders nothing rather than a field=empty-string query.
|
|
2046
|
+
const paramScoped = (activeFilter || []).some((f) => f && f.eqParam);
|
|
2047
|
+
const { rows } = paramScoped && PARAM === ""
|
|
2048
|
+
? { rows: [] }
|
|
2049
|
+
: await getJSON(dataUrl(data.source, data.table, activeFilter, data.orderBy));
|
|
2050
|
+
listEl.replaceChildren();
|
|
2051
|
+
if (!rows.length) { listEl.append(el("p", { class: "pc-prose-empty" }, p.empty || "No records")); return; }
|
|
2052
|
+
for (const row of rows) listEl.append(itemFor(row));
|
|
2053
|
+
} catch (e) {
|
|
2054
|
+
listEl.replaceChildren(el("p", { class: "pc-msg err" }, String((e && e.message) || e)));
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
document.addEventListener("pc:refresh", refresh);
|
|
2058
|
+
disposers.push(() => document.removeEventListener("pc:refresh", refresh));
|
|
2059
|
+
if (p.refreshMs && p.refreshMs > 0) {
|
|
2060
|
+
const timer = setInterval(refresh, p.refreshMs);
|
|
2061
|
+
disposers.push(() => clearInterval(timer));
|
|
2062
|
+
}
|
|
2063
|
+
refresh();
|
|
2064
|
+
return card;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
1738
2067
|
function renderDataGrid(node) {
|
|
1739
2068
|
const p = node.props;
|
|
1740
2069
|
const card = el("section", { class: "pc-card" });
|
|
@@ -1808,18 +2137,6 @@ function renderDataGrid(node) {
|
|
|
1808
2137
|
return v == null ? null : String(v);
|
|
1809
2138
|
};
|
|
1810
2139
|
|
|
1811
|
-
function dataUrl(source, tbl, filters, order) {
|
|
1812
|
-
let u = "/app/data/" + encodeURIComponent(source) + "/" + encodeURIComponent(tbl);
|
|
1813
|
-
const qs = [];
|
|
1814
|
-
for (const f of filters || []) {
|
|
1815
|
-
if (Array.isArray(f.in)) qs.push("where=" + encodeURIComponent(f.field + ":in:" + f.in.join(",")));
|
|
1816
|
-
else if (f.eqParam) qs.push("where=" + encodeURIComponent(f.field + ":" + PARAM));
|
|
1817
|
-
else qs.push("where=" + encodeURIComponent(f.field + ":" + f.eq));
|
|
1818
|
-
}
|
|
1819
|
-
if (order && order.field) qs.push("order=" + encodeURIComponent(order.field + ":" + (order.dir || "asc")));
|
|
1820
|
-
return qs.length ? u + "?" + qs.join("&") : u;
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
2140
|
async function fireAction(action, row) {
|
|
1824
2141
|
// Route-driven row action: POST the action's body template (default {})
|
|
1825
2142
|
// to its route path, with {{row.KEY}} tokens resolved from this row.
|
|
@@ -1831,14 +2148,18 @@ function renderDataGrid(node) {
|
|
|
1831
2148
|
const b = el("button", { class: "pc-btn pc-btn-sm" }, ra.label);
|
|
1832
2149
|
b.addEventListener("click", async (ev) => {
|
|
1833
2150
|
ev.stopPropagation();
|
|
1834
|
-
|
|
2151
|
+
// In-DOM confirm/error (openModal) instead of native confirm()/alert():
|
|
2152
|
+
// a sandboxed iframe without allow-modals makes native confirm() return
|
|
2153
|
+
// false and alert() a no-op, so every confirm-guarded action silently
|
|
2154
|
+
// dead-ended and its errors vanished (#276).
|
|
2155
|
+
if (ra.confirm && !(await confirmModal(ra.confirm))) return;
|
|
1835
2156
|
b.disabled = true;
|
|
1836
2157
|
try {
|
|
1837
2158
|
await fireAction(ra.action, row);
|
|
1838
2159
|
document.dispatchEvent(new CustomEvent("pc:refresh"));
|
|
1839
2160
|
} catch (e) {
|
|
1840
2161
|
b.disabled = false;
|
|
1841
|
-
|
|
2162
|
+
alertModal(String(e.message || e));
|
|
1842
2163
|
}
|
|
1843
2164
|
});
|
|
1844
2165
|
return b;
|
|
@@ -2225,7 +2546,7 @@ function renderNav(node) {
|
|
|
2225
2546
|
return nav;
|
|
2226
2547
|
}
|
|
2227
2548
|
|
|
2228
|
-
const RENDERERS = { text: renderText, actionForm: renderActionForm, dataGrid: renderDataGrid, nav: renderNav, button: renderButton };
|
|
2549
|
+
const RENDERERS = { text: renderText, actionForm: renderActionForm, dataGrid: renderDataGrid, prose: renderProse, nav: renderNav, button: renderButton };
|
|
2229
2550
|
|
|
2230
2551
|
// Durable per-node UI state. localStorage is keyed by the home page id + node
|
|
2231
2552
|
// id so two grids on the same page (or the same grid across pages) don't clash,
|
|
@@ -29,6 +29,7 @@ function ctxWith(surfaces: Record<string, unknown>): Parameters<typeof mountSurf
|
|
|
29
29
|
cancelInstance: async () => {},
|
|
30
30
|
publishMessage: async () => {},
|
|
31
31
|
searchUserTasks: async () => [],
|
|
32
|
+
openUserTasks: async () => [],
|
|
32
33
|
getForm: async () => null,
|
|
33
34
|
completeUserTask: async () => {},
|
|
34
35
|
searchProcessInstances: async () => [],
|
|
@@ -44,6 +45,7 @@ const fakeEngine: EngineClient = {
|
|
|
44
45
|
cancelInstance: async () => {},
|
|
45
46
|
publishMessage: async () => {},
|
|
46
47
|
searchUserTasks: async () => [],
|
|
48
|
+
openUserTasks: async () => [],
|
|
47
49
|
getForm: async () => null,
|
|
48
50
|
completeUserTask: async () => {},
|
|
49
51
|
searchProcessInstances: async () => [],
|
|
@@ -30,6 +30,7 @@ class MiniEngine implements EngineClient {
|
|
|
30
30
|
this.calls.push({ kind: "message", target: input.name, correlationKey: input.correlationKey, variables: input.variables });
|
|
31
31
|
}
|
|
32
32
|
async searchUserTasks(): Promise<[]> { return []; }
|
|
33
|
+
async openUserTasks(): Promise<[]> { return []; }
|
|
33
34
|
async getForm(): Promise<null> { return null; }
|
|
34
35
|
async completeUserTask(): Promise<void> {}
|
|
35
36
|
async searchProcessInstances(): Promise<[]> { return []; }
|
|
@@ -27,6 +27,7 @@ class MiniEngine implements EngineClient {
|
|
|
27
27
|
async cancelInstance(): Promise<void> {}
|
|
28
28
|
async publishMessage(): Promise<void> {}
|
|
29
29
|
async searchUserTasks(): Promise<[]> { return []; }
|
|
30
|
+
async openUserTasks(): Promise<[]> { return []; }
|
|
30
31
|
async getForm(): Promise<null> { return null; }
|
|
31
32
|
async completeUserTask(): Promise<void> {}
|
|
32
33
|
async searchProcessInstances(): Promise<[]> { return []; }
|