@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
|
@@ -216,6 +216,17 @@ export interface UserTaskSummary {
|
|
|
216
216
|
/** An external form reference (a form linked outside the deployment), if any. */
|
|
217
217
|
readonly externalFormReference?: string;
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* The lifecycle-state-agnostic selectors for a user-task search: which process instance,
|
|
221
|
+
* assignee, and/or candidate group to match. Shared by {@link EngineClient.searchUserTasks}
|
|
222
|
+
* (which adds an optional `state`) and {@link EngineClient.openUserTasks} (which pins
|
|
223
|
+
* `state: "CREATED"` for you), so the two accessors cannot drift on how a task is selected.
|
|
224
|
+
*/
|
|
225
|
+
export interface UserTaskFilter {
|
|
226
|
+
processInstanceKey?: string;
|
|
227
|
+
assignee?: string;
|
|
228
|
+
candidateGroup?: string;
|
|
229
|
+
}
|
|
219
230
|
/**
|
|
220
231
|
* A deployed form's form-js schema plus its identifying metadata, as
|
|
221
232
|
* {@link EngineClient.getForm} returns it. `schema` is the parsed form-js document
|
|
@@ -267,13 +278,28 @@ export interface EngineClient {
|
|
|
267
278
|
* By default this is unfiltered by lifecycle state and may return tasks in any state
|
|
268
279
|
* (e.g. completed/canceled); pass `state: "CREATED"` to constrain the search to open
|
|
269
280
|
* (answerable) tasks. Each result carries the task's resolved form linkage
|
|
270
|
-
* (`formKey`/`externalFormReference`) when present.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
281
|
+
* (`formKey`/`externalFormReference`) when present.
|
|
282
|
+
*
|
|
283
|
+
* **Footgun:** because this defaults to *any* lifecycle state and lags a completion,
|
|
284
|
+
* a just-answered task is still returned for a moment — so treating mere *existence*
|
|
285
|
+
* of a result as "open / actionable" projects a completed task as if a human could
|
|
286
|
+
* still act on it (a torn read-model row). {@link UserTaskSummary} deliberately omits
|
|
287
|
+
* `state`, so a caller cannot re-filter results defensively. If you are deciding
|
|
288
|
+
* whether a human can act — any reconcile / affordance path — use
|
|
289
|
+
* {@link EngineClient.openUserTasks} instead; reserve `searchUserTasks` for the rare
|
|
290
|
+
* legitimate "any lifecycle state" audit/read case. */
|
|
291
|
+
searchUserTasks(filter?: UserTaskFilter & {
|
|
275
292
|
state?: UserTaskState;
|
|
276
293
|
}): Promise<UserTaskSummary[]>;
|
|
294
|
+
/** The user tasks that are actually *open* (answerable) — semantically the accessor to
|
|
295
|
+
* reach for whenever you are deciding whether a human can act (reconcile / affordance
|
|
296
|
+
* paths). A thin, intent-named wrapper that pins the lifecycle-state invariant the SDK
|
|
297
|
+
* documents: exactly `searchUserTasks({ ...filter, state: "CREATED" })`, since `CREATED`
|
|
298
|
+
* is the only open state ({@link UserTaskState}). Unlike a bare `searchUserTasks`, it
|
|
299
|
+
* cannot surface a completed/canceled task as if it were still actionable — the footgun
|
|
300
|
+
* described on `searchUserTasks`. Same selectors as `searchUserTasks` minus `state`
|
|
301
|
+
* (pinned to `CREATED`). */
|
|
302
|
+
openUserTasks(filter?: UserTaskFilter): Promise<UserTaskSummary[]>;
|
|
277
303
|
/**
|
|
278
304
|
* Fetch a deployed form's form-js schema for the `taskInbox` surface. Resolve by
|
|
279
305
|
* `formKey` (the linkage the engine attaches to a user task) or, as a best-effort
|
|
@@ -575,6 +575,30 @@ table.pc-grid th { font-weight:600; color:var(--nano-text-muted); }
|
|
|
575
575
|
.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; }
|
|
576
576
|
.pc-group-toggle:hover { background:var(--nano-hover); }
|
|
577
577
|
.pc-group-title { font-size:.9rem; }
|
|
578
|
+
/* Data-bound prose/markdown list (renderProse, #274). Long-form narrative
|
|
579
|
+
records — plan-review findings, coordination notes, escalation bodies — read
|
|
580
|
+
as a stacked, full-width document at a comfortable measure (~66ch) instead of
|
|
581
|
+
being jammed into a grid column. Each item stacks a small header over a
|
|
582
|
+
markdown-rendered body; the body max-width is set inline per node (props.measure)
|
|
583
|
+
with this rule as the no-JS fallback. */
|
|
584
|
+
.pc-prose-list { display:flex; flex-direction:column; gap:1.1rem; }
|
|
585
|
+
.pc-prose-item { border-top:1px solid var(--nano-edge); padding-top:.9rem; }
|
|
586
|
+
.pc-prose-item:first-child { border-top:0; padding-top:0; }
|
|
587
|
+
.pc-prose-head { font-size:.8rem; font-weight:600; color:var(--nano-text-muted); text-transform:uppercase; letter-spacing:.03em; margin:0 0 .35rem; }
|
|
588
|
+
.pc-prose-empty { color:var(--nano-text-faint); }
|
|
589
|
+
.pc-prose-body { max-width:66ch; }
|
|
590
|
+
.pc-prose-body > :first-child { margin-top:0; }
|
|
591
|
+
.pc-prose-body > :last-child { margin-bottom:0; }
|
|
592
|
+
.pc-md-p { margin:.55rem 0; overflow-wrap:anywhere; }
|
|
593
|
+
.pc-md-h { font-size:1.05rem; font-weight:650; margin:.95rem 0 .4rem; }
|
|
594
|
+
.pc-md-list { margin:.45rem 0; padding-left:1.35rem; }
|
|
595
|
+
.pc-md-list li { margin:.15rem 0; }
|
|
596
|
+
.pc-md-quote { margin:.6rem 0; padding:.1rem .9rem; border-left:3px solid var(--nano-edge-strong); color:var(--nano-text-muted); }
|
|
597
|
+
.pc-md-pre { background:var(--nano-inset); border:1px solid var(--nano-edge); border-radius:.4rem; padding:.7rem .8rem; overflow:auto; margin:.6rem 0; }
|
|
598
|
+
.pc-md-pre code { font:.82rem/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--nano-text); white-space:pre; }
|
|
599
|
+
.pc-md-code { background:var(--nano-inset); border-radius:.3rem; padding:.05rem .3rem; font:.85em ui-monospace,SFMono-Regular,Menlo,monospace; }
|
|
600
|
+
.pc-md-hr { border:0; border-top:1px solid var(--nano-edge); margin:1rem 0; }
|
|
601
|
+
.pc-prose-body a { color:var(--nano-accent-strong); }
|
|
578
602
|
/* A standalone button node + the modal it opens (e.g. a copy-pasteable prompt). */
|
|
579
603
|
.pc-buttonrow { margin:1rem 0; }
|
|
580
604
|
.pc-btn-ghost { background:transparent; color:var(--nano-text-muted); border:1px solid var(--nano-edge); }
|
|
@@ -682,8 +706,8 @@ table.pc-grid th { font-weight:600; color:var(--nano-text-muted); }
|
|
|
682
706
|
`;
|
|
683
707
|
// The schema-driven browser renderer (ADR 0042 §3). Plain ES module string served at
|
|
684
708
|
// /app/runtime.js — it does NOT ship Craft.js (authoring is console-side only). It
|
|
685
|
-
// fetches the home page's page.json and renders text / actionForm / dataGrid
|
|
686
|
-
// wiring actionForm → /app/actions/start and dataGrid → /app/data (with a refresh).
|
|
709
|
+
// fetches the home page's page.json and renders text / actionForm / dataGrid / prose
|
|
710
|
+
// nodes, wiring actionForm → /app/actions/start and dataGrid/prose → /app/data (with a refresh).
|
|
687
711
|
const RENDERER_JS = String.raw `
|
|
688
712
|
const root = document.getElementById("page");
|
|
689
713
|
const HOME = root.dataset.home || "home";
|
|
@@ -991,7 +1015,9 @@ function resolveCopyText(text) {
|
|
|
991
1015
|
function openModal(m) {
|
|
992
1016
|
// Only one modal at a time — drop a second (e.g. double-click) open so we
|
|
993
1017
|
// never stack overlays or register duplicate document-level keydown handlers.
|
|
994
|
-
|
|
1018
|
+
// Return null (not a close fn) so a guarded caller (confirmModal) can tell the
|
|
1019
|
+
// open was dropped and resolve rather than hang.
|
|
1020
|
+
if (modalOpen) return null;
|
|
995
1021
|
modalOpen = true;
|
|
996
1022
|
const overlay = el("div", { class: "pc-modal-overlay" });
|
|
997
1023
|
// role="dialog" + aria-modal need an accessible name: label by the visible
|
|
@@ -1010,6 +1036,13 @@ function openModal(m) {
|
|
|
1010
1036
|
// Remember what had focus so we can restore it on close — a keyboard/screen
|
|
1011
1037
|
// reader user returns to where they were instead of being dumped at <body>.
|
|
1012
1038
|
const prevFocus = document.activeElement;
|
|
1039
|
+
// A dismissal (✕/Close button, backdrop, Escape, page switch) resolves with
|
|
1040
|
+
// m.dismissValue; an action button overrides it before closing. onResult fires
|
|
1041
|
+
// exactly once, from close(), so every exit path (button OR dismiss) reports a
|
|
1042
|
+
// result — this is what lets confirmModal() gate on a real user choice inside a
|
|
1043
|
+
// sandboxed iframe where native confirm() would silently return false (#276).
|
|
1044
|
+
const onResult = typeof m.onResult === "function" ? m.onResult : null;
|
|
1045
|
+
let resultValue = m.dismissValue;
|
|
1013
1046
|
let closed = false;
|
|
1014
1047
|
function close() {
|
|
1015
1048
|
if (closed) return;
|
|
@@ -1022,6 +1055,9 @@ function openModal(m) {
|
|
|
1022
1055
|
// accrete dead close() closures until the next page navigation runs teardown.
|
|
1023
1056
|
const i = disposers.indexOf(close);
|
|
1024
1057
|
if (i !== -1) disposers.splice(i, 1);
|
|
1058
|
+
// A consumer-provided result callback must never abort teardown: if it throws,
|
|
1059
|
+
// swallow it so focus restoration below (and Escape/backdrop close) still runs.
|
|
1060
|
+
if (onResult) { try { onResult(resultValue); } catch (_e) {} }
|
|
1025
1061
|
if (prevFocus && typeof prevFocus.focus === "function") prevFocus.focus();
|
|
1026
1062
|
}
|
|
1027
1063
|
// Focusable descendants of the dialog, in DOM order, for the Tab focus trap.
|
|
@@ -1054,7 +1090,29 @@ function openModal(m) {
|
|
|
1054
1090
|
}
|
|
1055
1091
|
const closeBtn = el("button", { class: "pc-btn pc-btn-sm pc-btn-ghost", type: "button" }, m.closeLabel || "Close");
|
|
1056
1092
|
closeBtn.addEventListener("click", close);
|
|
1057
|
-
actions
|
|
1093
|
+
// Custom action buttons (m.actions) turn this into a decision dialog — each
|
|
1094
|
+
// sets the result then closes; the plain Close button is the default when none
|
|
1095
|
+
// are given. This is the in-DOM replacement for native confirm()/alert(): one
|
|
1096
|
+
// overlay + focus-trap + teardown machinery, no drift (AGENTS.md).
|
|
1097
|
+
let focusBtn = closeBtn;
|
|
1098
|
+
if (Array.isArray(m.actions) && m.actions.length) {
|
|
1099
|
+
let firstActionBtn = null;
|
|
1100
|
+
let defaultBtn = null;
|
|
1101
|
+
for (const a of m.actions) {
|
|
1102
|
+
const cls = "pc-btn pc-btn-sm" + (a.variant === "ghost" ? " pc-btn-ghost" : "");
|
|
1103
|
+
const ab = el("button", { class: cls, type: "button" }, a.label);
|
|
1104
|
+
ab.addEventListener("click", () => { resultValue = a.value; close(); });
|
|
1105
|
+
actions.append(ab);
|
|
1106
|
+
if (!firstActionBtn) firstActionBtn = ab;
|
|
1107
|
+
if (a.default) defaultBtn = ab;
|
|
1108
|
+
}
|
|
1109
|
+
// Default focus lands on the FIRST action (typically Cancel), never the last:
|
|
1110
|
+
// for confirmModal that keeps a stray Enter from accidentally triggering the
|
|
1111
|
+
// destructive Confirm. An action can opt into autofocus with default:true.
|
|
1112
|
+
focusBtn = defaultBtn || firstActionBtn;
|
|
1113
|
+
} else {
|
|
1114
|
+
actions.append(closeBtn);
|
|
1115
|
+
}
|
|
1058
1116
|
dialog.append(actions);
|
|
1059
1117
|
overlay.append(dialog);
|
|
1060
1118
|
// A click on the backdrop (never the dialog itself) dismisses.
|
|
@@ -1064,7 +1122,36 @@ function openModal(m) {
|
|
|
1064
1122
|
// A page switch runs teardown() before the next render — dispose the modal
|
|
1065
1123
|
// there too so navigating away never leaves a stale overlay + keydown listener.
|
|
1066
1124
|
disposers.push(close);
|
|
1067
|
-
|
|
1125
|
+
focusBtn.focus();
|
|
1126
|
+
return close;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// In-DOM confirm — an accessible replacement for native confirm() that works in
|
|
1130
|
+
// a sandboxed iframe (without allow-modals native confirm() silently returns
|
|
1131
|
+
// false, dead-ending every confirm-guarded action; #276). Renders the message
|
|
1132
|
+
// with Cancel/Confirm and resolves the user's choice; a dismissal (Escape /
|
|
1133
|
+
// backdrop / ✕) resolves false. If a modal is already open the guarded open is
|
|
1134
|
+
// dropped — resolve false so the caller doesn't hang.
|
|
1135
|
+
function confirmModal(message) {
|
|
1136
|
+
return new Promise((resolve) => {
|
|
1137
|
+
const opened = openModal({
|
|
1138
|
+
title: "Confirm",
|
|
1139
|
+
description: String(message),
|
|
1140
|
+
dismissValue: false,
|
|
1141
|
+
actions: [
|
|
1142
|
+
{ label: "Cancel", value: false, variant: "ghost" },
|
|
1143
|
+
{ label: "Confirm", value: true },
|
|
1144
|
+
],
|
|
1145
|
+
onResult: (v) => resolve(!!v),
|
|
1146
|
+
});
|
|
1147
|
+
if (!opened) resolve(false);
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// In-DOM error surface — replaces native alert(), which a sandboxed iframe also
|
|
1152
|
+
// suppresses, so action failures reported through it were invisible (#276).
|
|
1153
|
+
function alertModal(message) {
|
|
1154
|
+
openModal({ title: "Error", description: String(message) });
|
|
1068
1155
|
}
|
|
1069
1156
|
|
|
1070
1157
|
// A standalone button node. Clicking it opens props.modal — used e.g. to surface
|
|
@@ -1682,6 +1769,248 @@ function renderActionForm(node) {
|
|
|
1682
1769
|
// Single source of truth for both the initial render and every refresh below.
|
|
1683
1770
|
function rowCountLabel(n) { return n + (n === 1 ? " row" : " rows"); }
|
|
1684
1771
|
|
|
1772
|
+
// Build the /app/data/<source>/<table> URL a data-bound list renderer fetches,
|
|
1773
|
+
// threading the whitelisted where/order query params. Shared by dataGrid and the
|
|
1774
|
+
// prose renderer so the two can't drift on how a filter/orderBy is encoded. A
|
|
1775
|
+
// filter is { field, in:[…] } (IN set), { field, eqParam:true } (bound to the
|
|
1776
|
+
// current route PARAM — "show the selected entity's rows"), or { field, eq } (a
|
|
1777
|
+
// literal equals); orderBy is { field, dir }. The source/table/field names are
|
|
1778
|
+
// server-side whitelisted (the /app/data route rejects unknown columns).
|
|
1779
|
+
function dataUrl(source, tbl, filters, order) {
|
|
1780
|
+
let u = "/app/data/" + encodeURIComponent(source) + "/" + encodeURIComponent(tbl);
|
|
1781
|
+
const qs = [];
|
|
1782
|
+
for (const f of filters || []) {
|
|
1783
|
+
if (Array.isArray(f.in)) qs.push("where=" + encodeURIComponent(f.field + ":in:" + f.in.join(",")));
|
|
1784
|
+
else if (f.eqParam) qs.push("where=" + encodeURIComponent(f.field + ":" + PARAM));
|
|
1785
|
+
else qs.push("where=" + encodeURIComponent(f.field + ":" + f.eq));
|
|
1786
|
+
}
|
|
1787
|
+
if (order && order.field) qs.push("order=" + encodeURIComponent(order.field + ":" + (order.dir || "asc")));
|
|
1788
|
+
return qs.length ? u + "?" + qs.join("&") : u;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// ── Minimal, safe Markdown → DOM (no innerHTML) — used by the prose renderer ──
|
|
1792
|
+
// A dependency-free CommonMark subset. It NEVER sets innerHTML: every block and
|
|
1793
|
+
// inline token is materialised as a known element via el(), with text set through
|
|
1794
|
+
// textContent / createTextNode — so a record's body field may carry markdown
|
|
1795
|
+
// WITHOUT ever becoming an HTML/script injection vector (the same text-only
|
|
1796
|
+
// guarantee interpTemplate gives a grid cell). Supported: ATX headings
|
|
1797
|
+
// (#..######), fenced + inline code, bold (**/__), italic (*/_), links
|
|
1798
|
+
// [text](url) (http/https/mailto only — every other scheme degrades to its plain
|
|
1799
|
+
// label), unordered (-,*,+) and ordered (1. / 1)) lists, blockquotes (>),
|
|
1800
|
+
// thematic breaks (---), and blank-line-separated paragraphs. The backtick used
|
|
1801
|
+
// for code spans/fences is built from a char code so this source stays inside the
|
|
1802
|
+
// String.raw template literal (which is itself delimited by a backtick).
|
|
1803
|
+
const MD_BACKTICK = String.fromCharCode(96);
|
|
1804
|
+
function mdIsWordChar(c) { return typeof c === "string" && /[0-9A-Za-z]/.test(c); }
|
|
1805
|
+
// Only http(s)/mailto hrefs become anchors; anything else (javascript:, data:,
|
|
1806
|
+
// vbscript:, a bare relative path, …) returns "" so the caller renders the label
|
|
1807
|
+
// as plain text rather than a clickable, potentially-hostile link.
|
|
1808
|
+
function mdSafeHref(href) {
|
|
1809
|
+
const h = String(href == null ? "" : href).trim();
|
|
1810
|
+
return /^(?:https?:|mailto:)/i.test(h) ? h : "";
|
|
1811
|
+
}
|
|
1812
|
+
// Find the closing run of 'marker' at or after 'from'. For underscore emphasis the
|
|
1813
|
+
// close must not be intra-word (a following alphanumeric means snake_case, not a
|
|
1814
|
+
// delimiter), so keep scanning past such runs; -1 when there is no valid close.
|
|
1815
|
+
function mdFindClose(s, marker, from, underscore) {
|
|
1816
|
+
let j = from;
|
|
1817
|
+
while (true) {
|
|
1818
|
+
const k = s.indexOf(marker, j);
|
|
1819
|
+
if (k < 0) return -1;
|
|
1820
|
+
if (!underscore || !mdIsWordChar(s[k + marker.length])) return k;
|
|
1821
|
+
j = k + marker.length;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
function mdInline(text) {
|
|
1825
|
+
const s = String(text == null ? "" : text);
|
|
1826
|
+
const nodes = [];
|
|
1827
|
+
let buf = "";
|
|
1828
|
+
const flush = () => { if (buf) { nodes.push(document.createTextNode(buf)); buf = ""; } };
|
|
1829
|
+
let i = 0;
|
|
1830
|
+
while (i < s.length) {
|
|
1831
|
+
const ch = s[i];
|
|
1832
|
+
// Backslash escape: the next char is emitted literally, never as a delimiter.
|
|
1833
|
+
if (ch === "\\" && i + 1 < s.length) { buf += s[i + 1]; i += 2; continue; }
|
|
1834
|
+
// Inline code span — verbatim, no nested inline parsing.
|
|
1835
|
+
if (ch === MD_BACKTICK) {
|
|
1836
|
+
const end = s.indexOf(MD_BACKTICK, i + 1);
|
|
1837
|
+
if (end > i) { flush(); nodes.push(el("code", { class: "pc-md-code" }, s.slice(i + 1, end))); i = end + 1; continue; }
|
|
1838
|
+
}
|
|
1839
|
+
// Strong (** or __). Underscore only when it opens on a word boundary so an
|
|
1840
|
+
// identifier like a__b doesn't get mangled into emphasis.
|
|
1841
|
+
if ((ch === "*" || ch === "_") && s[i + 1] === ch) {
|
|
1842
|
+
const underscore = ch === "_";
|
|
1843
|
+
if (!underscore || !mdIsWordChar(s[i - 1])) {
|
|
1844
|
+
const marker = ch + ch;
|
|
1845
|
+
const end = mdFindClose(s, marker, i + 2, underscore);
|
|
1846
|
+
if (end > i + 1) { flush(); nodes.push(el("strong", {}, ...mdInline(s.slice(i + 2, end)))); i = end + 2; continue; }
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
// Emphasis (* or _), same word-boundary guard for underscore.
|
|
1850
|
+
if (ch === "*" || ch === "_") {
|
|
1851
|
+
const underscore = ch === "_";
|
|
1852
|
+
if (!underscore || !mdIsWordChar(s[i - 1])) {
|
|
1853
|
+
const end = mdFindClose(s, ch, i + 1, underscore);
|
|
1854
|
+
if (end > i + 1) { flush(); nodes.push(el("em", {}, ...mdInline(s.slice(i + 1, end)))); i = end + 1; continue; }
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
// Link [label](href): href sanitised; an unsafe scheme drops to the label text.
|
|
1858
|
+
if (ch === "[") {
|
|
1859
|
+
const close = s.indexOf("]", i + 1);
|
|
1860
|
+
if (close > i && s[close + 1] === "(") {
|
|
1861
|
+
const paren = s.indexOf(")", close + 2);
|
|
1862
|
+
if (paren > close) {
|
|
1863
|
+
const href = mdSafeHref(s.slice(close + 2, paren));
|
|
1864
|
+
const label = mdInline(s.slice(i + 1, close));
|
|
1865
|
+
flush();
|
|
1866
|
+
if (href) nodes.push(el("a", { href: href, target: "_blank", rel: "noopener noreferrer" }, ...label));
|
|
1867
|
+
else for (const l of label) nodes.push(l);
|
|
1868
|
+
i = paren + 1; continue;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
buf += ch; i++;
|
|
1873
|
+
}
|
|
1874
|
+
flush();
|
|
1875
|
+
return nodes;
|
|
1876
|
+
}
|
|
1877
|
+
// Regexes that classify a block-opening line; shared by the block loop and the
|
|
1878
|
+
// paragraph terminator so the two can't disagree on where a paragraph ends.
|
|
1879
|
+
const MD_HEADING = /^(#{1,6})\s+(.*)$/;
|
|
1880
|
+
const MD_QUOTE = /^>\s?/;
|
|
1881
|
+
const MD_UL = /^[-*+]\s+/;
|
|
1882
|
+
const MD_OL = /^\d+[.)]\s+/;
|
|
1883
|
+
const MD_HR = /^([-*_])(?:\s*\1){2,}\s*$/;
|
|
1884
|
+
function mdBlockStart(t) {
|
|
1885
|
+
return t.slice(0, 3) === MD_BACKTICK + MD_BACKTICK + MD_BACKTICK
|
|
1886
|
+
|| MD_HEADING.test(t) || MD_QUOTE.test(t) || MD_UL.test(t) || MD_OL.test(t) || MD_HR.test(t);
|
|
1887
|
+
}
|
|
1888
|
+
// Parse a markdown string into an array of block-level DOM nodes.
|
|
1889
|
+
function mdToNodes(src) {
|
|
1890
|
+
const out = [];
|
|
1891
|
+
const fence = MD_BACKTICK + MD_BACKTICK + MD_BACKTICK;
|
|
1892
|
+
const lines = String(src == null ? "" : src).replace(/\r\n?/g, "\n").split("\n");
|
|
1893
|
+
let i = 0;
|
|
1894
|
+
while (i < lines.length) {
|
|
1895
|
+
const t = lines[i].trim();
|
|
1896
|
+
if (t === "") { i++; continue; }
|
|
1897
|
+
// Fenced code block — content is verbatim (no inline parsing).
|
|
1898
|
+
if (t.slice(0, 3) === fence) {
|
|
1899
|
+
const body = [];
|
|
1900
|
+
i++;
|
|
1901
|
+
while (i < lines.length && lines[i].trim().slice(0, 3) !== fence) { body.push(lines[i]); i++; }
|
|
1902
|
+
if (i < lines.length) i++; // consume the closing fence
|
|
1903
|
+
out.push(el("pre", { class: "pc-md-pre" }, el("code", {}, body.join("\n"))));
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
// Thematic break (checked before lists so "***"/"---" isn't read as a bullet).
|
|
1907
|
+
if (MD_HR.test(t)) { out.push(el("hr", { class: "pc-md-hr" })); i++; continue; }
|
|
1908
|
+
// ATX heading.
|
|
1909
|
+
const h = MD_HEADING.exec(t);
|
|
1910
|
+
if (h) { out.push(el("h" + h[1].length, { class: "pc-md-h" }, ...mdInline(h[2].trim()))); i++; continue; }
|
|
1911
|
+
// Blockquote — strip one '>' per line and re-parse the inner block(s).
|
|
1912
|
+
if (MD_QUOTE.test(t)) {
|
|
1913
|
+
const inner = [];
|
|
1914
|
+
while (i < lines.length && MD_QUOTE.test(lines[i].trim())) { inner.push(lines[i].trim().replace(MD_QUOTE, "")); i++; }
|
|
1915
|
+
const bq = el("blockquote", { class: "pc-md-quote" });
|
|
1916
|
+
for (const b of mdToNodes(inner.join("\n"))) bq.append(b);
|
|
1917
|
+
out.push(bq); continue;
|
|
1918
|
+
}
|
|
1919
|
+
// Unordered list.
|
|
1920
|
+
if (MD_UL.test(t)) {
|
|
1921
|
+
const ul = el("ul", { class: "pc-md-list" });
|
|
1922
|
+
while (i < lines.length && MD_UL.test(lines[i].trim())) {
|
|
1923
|
+
ul.append(el("li", {}, ...mdInline(lines[i].trim().replace(MD_UL, ""))));
|
|
1924
|
+
i++;
|
|
1925
|
+
}
|
|
1926
|
+
out.push(ul); continue;
|
|
1927
|
+
}
|
|
1928
|
+
// Ordered list.
|
|
1929
|
+
if (MD_OL.test(t)) {
|
|
1930
|
+
const ol = el("ol", { class: "pc-md-list" });
|
|
1931
|
+
while (i < lines.length && MD_OL.test(lines[i].trim())) {
|
|
1932
|
+
ol.append(el("li", {}, ...mdInline(lines[i].trim().replace(MD_OL, ""))));
|
|
1933
|
+
i++;
|
|
1934
|
+
}
|
|
1935
|
+
out.push(ol); continue;
|
|
1936
|
+
}
|
|
1937
|
+
// Paragraph — soft-wrapped lines joined with a space until a blank line or the
|
|
1938
|
+
// next block opener.
|
|
1939
|
+
const para = [];
|
|
1940
|
+
while (i < lines.length) {
|
|
1941
|
+
const lt = lines[i].trim();
|
|
1942
|
+
if (lt === "" || mdBlockStart(lt)) break;
|
|
1943
|
+
para.push(lt); i++;
|
|
1944
|
+
}
|
|
1945
|
+
out.push(el("p", { class: "pc-md-p" }, ...mdInline(para.join(" "))));
|
|
1946
|
+
}
|
|
1947
|
+
return out;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// A data-bound prose/markdown list (#274). Binds a datasource like dataGrid
|
|
1951
|
+
// (props.data = { source, table, orderBy, filter }) but renders each row as a
|
|
1952
|
+
// stacked prose block — a small header template (props.header, e.g.
|
|
1953
|
+
// "Round {{round}} · {{approved}}") over one body field (props.body) rendered as
|
|
1954
|
+
// sanitised markdown at a comfortable measure (props.measure ch, ~66 default).
|
|
1955
|
+
// This is the primitive for narrative records (plan-review findings, coordination
|
|
1956
|
+
// notes, escalation bodies) that read as a document, not a squeezed table cell.
|
|
1957
|
+
// It inherits collapsible/defaultCollapsed for free via makeCollapsible (which
|
|
1958
|
+
// unwraps the <h2> title), and refreshMs polling + pc:refresh like dataGrid.
|
|
1959
|
+
function renderProse(node) {
|
|
1960
|
+
const p = node.props || {};
|
|
1961
|
+
const data = p.data || {};
|
|
1962
|
+
const card = el("section", { class: "pc-card" });
|
|
1963
|
+
if (p.title) card.append(el("h2", {}, p.title));
|
|
1964
|
+
const listEl = el("div", { class: "pc-prose-list" });
|
|
1965
|
+
card.append(listEl);
|
|
1966
|
+
const headerTpl = typeof p.header === "string" ? p.header : null;
|
|
1967
|
+
const bodyField = p.body;
|
|
1968
|
+
// Clamp the reading measure to a sane range so a malformed schema can't set a
|
|
1969
|
+
// 5000ch (full-bleed) or 2ch (one-word-per-line) body. ~66ch is the default.
|
|
1970
|
+
let measure = Number(p.measure);
|
|
1971
|
+
if (!Number.isFinite(measure)) measure = 66;
|
|
1972
|
+
measure = Math.max(40, Math.min(100, Math.round(measure)));
|
|
1973
|
+
const activeFilter = Array.isArray(data.filter) ? data.filter : [];
|
|
1974
|
+
|
|
1975
|
+
function itemFor(row) {
|
|
1976
|
+
const item = el("article", { class: "pc-prose-item" });
|
|
1977
|
+
if (headerTpl) item.append(el("div", { class: "pc-prose-head" }, interpTemplate(headerTpl, row)));
|
|
1978
|
+
const bodyEl = el("div", { class: "pc-prose-body", style: "max-width:" + measure + "ch" });
|
|
1979
|
+
// Own-property gate (matching interpTemplate) so a body field name that
|
|
1980
|
+
// collides with a prototype key can't pick up prototype cruft.
|
|
1981
|
+
const raw = bodyField != null && Object.prototype.hasOwnProperty.call(row, bodyField) && row[bodyField] != null
|
|
1982
|
+
? String(row[bodyField])
|
|
1983
|
+
: "";
|
|
1984
|
+
for (const b of mdToNodes(raw)) bodyEl.append(b);
|
|
1985
|
+
item.append(bodyEl);
|
|
1986
|
+
return item;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
async function refresh() {
|
|
1990
|
+
try {
|
|
1991
|
+
// A param-scoped list ("show the selected entity's records") with no route
|
|
1992
|
+
// param present renders nothing rather than a field=empty-string query.
|
|
1993
|
+
const paramScoped = (activeFilter || []).some((f) => f && f.eqParam);
|
|
1994
|
+
const { rows } = paramScoped && PARAM === ""
|
|
1995
|
+
? { rows: [] }
|
|
1996
|
+
: await getJSON(dataUrl(data.source, data.table, activeFilter, data.orderBy));
|
|
1997
|
+
listEl.replaceChildren();
|
|
1998
|
+
if (!rows.length) { listEl.append(el("p", { class: "pc-prose-empty" }, p.empty || "No records")); return; }
|
|
1999
|
+
for (const row of rows) listEl.append(itemFor(row));
|
|
2000
|
+
} catch (e) {
|
|
2001
|
+
listEl.replaceChildren(el("p", { class: "pc-msg err" }, String((e && e.message) || e)));
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
document.addEventListener("pc:refresh", refresh);
|
|
2005
|
+
disposers.push(() => document.removeEventListener("pc:refresh", refresh));
|
|
2006
|
+
if (p.refreshMs && p.refreshMs > 0) {
|
|
2007
|
+
const timer = setInterval(refresh, p.refreshMs);
|
|
2008
|
+
disposers.push(() => clearInterval(timer));
|
|
2009
|
+
}
|
|
2010
|
+
refresh();
|
|
2011
|
+
return card;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
1685
2014
|
function renderDataGrid(node) {
|
|
1686
2015
|
const p = node.props;
|
|
1687
2016
|
const card = el("section", { class: "pc-card" });
|
|
@@ -1755,18 +2084,6 @@ function renderDataGrid(node) {
|
|
|
1755
2084
|
return v == null ? null : String(v);
|
|
1756
2085
|
};
|
|
1757
2086
|
|
|
1758
|
-
function dataUrl(source, tbl, filters, order) {
|
|
1759
|
-
let u = "/app/data/" + encodeURIComponent(source) + "/" + encodeURIComponent(tbl);
|
|
1760
|
-
const qs = [];
|
|
1761
|
-
for (const f of filters || []) {
|
|
1762
|
-
if (Array.isArray(f.in)) qs.push("where=" + encodeURIComponent(f.field + ":in:" + f.in.join(",")));
|
|
1763
|
-
else if (f.eqParam) qs.push("where=" + encodeURIComponent(f.field + ":" + PARAM));
|
|
1764
|
-
else qs.push("where=" + encodeURIComponent(f.field + ":" + f.eq));
|
|
1765
|
-
}
|
|
1766
|
-
if (order && order.field) qs.push("order=" + encodeURIComponent(order.field + ":" + (order.dir || "asc")));
|
|
1767
|
-
return qs.length ? u + "?" + qs.join("&") : u;
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
2087
|
async function fireAction(action, row) {
|
|
1771
2088
|
// Route-driven row action: POST the action's body template (default {})
|
|
1772
2089
|
// to its route path, with {{row.KEY}} tokens resolved from this row.
|
|
@@ -1778,14 +2095,18 @@ function renderDataGrid(node) {
|
|
|
1778
2095
|
const b = el("button", { class: "pc-btn pc-btn-sm" }, ra.label);
|
|
1779
2096
|
b.addEventListener("click", async (ev) => {
|
|
1780
2097
|
ev.stopPropagation();
|
|
1781
|
-
|
|
2098
|
+
// In-DOM confirm/error (openModal) instead of native confirm()/alert():
|
|
2099
|
+
// a sandboxed iframe without allow-modals makes native confirm() return
|
|
2100
|
+
// false and alert() a no-op, so every confirm-guarded action silently
|
|
2101
|
+
// dead-ended and its errors vanished (#276).
|
|
2102
|
+
if (ra.confirm && !(await confirmModal(ra.confirm))) return;
|
|
1782
2103
|
b.disabled = true;
|
|
1783
2104
|
try {
|
|
1784
2105
|
await fireAction(ra.action, row);
|
|
1785
2106
|
document.dispatchEvent(new CustomEvent("pc:refresh"));
|
|
1786
2107
|
} catch (e) {
|
|
1787
2108
|
b.disabled = false;
|
|
1788
|
-
|
|
2109
|
+
alertModal(String(e.message || e));
|
|
1789
2110
|
}
|
|
1790
2111
|
});
|
|
1791
2112
|
return b;
|
|
@@ -2172,7 +2493,7 @@ function renderNav(node) {
|
|
|
2172
2493
|
return nav;
|
|
2173
2494
|
}
|
|
2174
2495
|
|
|
2175
|
-
const RENDERERS = { text: renderText, actionForm: renderActionForm, dataGrid: renderDataGrid, nav: renderNav, button: renderButton };
|
|
2496
|
+
const RENDERERS = { text: renderText, actionForm: renderActionForm, dataGrid: renderDataGrid, prose: renderProse, nav: renderNav, button: renderButton };
|
|
2176
2497
|
|
|
2177
2498
|
// Durable per-node UI state. localStorage is keyed by the home page id + node
|
|
2178
2499
|
// id so two grids on the same page (or the same grid across pages) don't clash,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EngineClient, FormSchema, JobHandler, ProcessInstanceSnapshot, ProcessInstanceState, UserTaskState, UserTaskSummary, WorkerSubscription } from "../core/host.ts";
|
|
1
|
+
import type { EngineClient, FormSchema, JobHandler, ProcessInstanceSnapshot, ProcessInstanceState, UserTaskState, UserTaskSummary, UserTaskFilter, WorkerSubscription } from "../core/host.ts";
|
|
2
2
|
import type { EngineSdkClient } from "./sdk.ts";
|
|
3
3
|
/** Coerce an engine response's process-instance key to a non-empty string, or
|
|
4
4
|
* throw — a missing key means a malformed/partial response, not a real instance. */
|
|
@@ -136,12 +136,13 @@ export declare class SdkEngineClient implements EngineClient {
|
|
|
136
136
|
correlationKey?: string;
|
|
137
137
|
variables?: Record<string, unknown>;
|
|
138
138
|
}): Promise<void>;
|
|
139
|
-
searchUserTasks(filter?: {
|
|
140
|
-
processInstanceKey?: string;
|
|
141
|
-
assignee?: string;
|
|
142
|
-
candidateGroup?: string;
|
|
139
|
+
searchUserTasks(filter?: UserTaskFilter & {
|
|
143
140
|
state?: UserTaskState;
|
|
144
141
|
}): Promise<UserTaskSummary[]>;
|
|
142
|
+
/** The open (answerable) user tasks — `searchUserTasks` pinned to `state: "CREATED"`.
|
|
143
|
+
* The single safe accessor for reconcile/affordance paths (see `EngineClient.openUserTasks`);
|
|
144
|
+
* derives from `searchUserTasks` so the two cannot drift. */
|
|
145
|
+
openUserTasks(filter?: UserTaskFilter): Promise<UserTaskSummary[]>;
|
|
145
146
|
getForm(input: {
|
|
146
147
|
formKey?: string;
|
|
147
148
|
formId?: string;
|
|
@@ -127,6 +127,12 @@ export class SdkEngineClient {
|
|
|
127
127
|
}];
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
+
/** The open (answerable) user tasks — `searchUserTasks` pinned to `state: "CREATED"`.
|
|
131
|
+
* The single safe accessor for reconcile/affordance paths (see `EngineClient.openUserTasks`);
|
|
132
|
+
* derives from `searchUserTasks` so the two cannot drift. */
|
|
133
|
+
openUserTasks(filter) {
|
|
134
|
+
return this.searchUserTasks({ ...filter, state: "CREATED" });
|
|
135
|
+
}
|
|
130
136
|
async getForm(input) {
|
|
131
137
|
// The engine addresses deployed forms by key (`GET /forms/{formKey}`). A user task's
|
|
132
138
|
// `formKey` is the engine's resolution of its `formId` linkage to the latest deployed
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { bindModeToHost, expandEnv, expandEnvString, isBindMode, loadManifest, p
|
|
|
8
8
|
export type { ActionDecl, AppManifest, BindMode, ChatSurface, DataSource, DomainField, DomainType, LlmBinding, NetworkConfig, PagesSurface, Surfaces, TaskInboxSurface, Trigger, Worker, } from "./core/manifest.ts";
|
|
9
9
|
export { collectManifestIssues, ManifestValidationError, validateManifest, } from "./core/validate.ts";
|
|
10
10
|
export type { ValidationIssue } from "./core/validate.ts";
|
|
11
|
-
export type { EngineClient, EngineJob, FormSchema, HostContext, HttpHandler, HttpRequest, HttpResponse, HttpServer, JobHandler, ProcessInstanceSnapshot, ProcessInstanceState, SqliteDb, UserTaskState, UserTaskSummary, WatchHandle, WorkerSubscription, } from "./core/host.ts";
|
|
11
|
+
export type { EngineClient, EngineJob, FormSchema, HostContext, HttpHandler, HttpRequest, HttpResponse, HttpServer, JobHandler, ProcessInstanceSnapshot, ProcessInstanceState, SqliteDb, UserTaskState, UserTaskSummary, UserTaskFilter, WatchHandle, WorkerSubscription, } from "./core/host.ts";
|
|
12
12
|
export { buildFormSchema, parseFormSchema, pickFormLinkage, presentFormIdentifier, resolveFormIdentifier, } from "./core/form-contract.ts";
|
|
13
13
|
export type { AppApi, Mounted, RuntimeContext } from "./core/context.ts";
|
|
14
14
|
export { createLogger } from "./core/logger.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/src/runtime/core/host.ts
CHANGED
|
@@ -249,6 +249,18 @@ export interface UserTaskSummary {
|
|
|
249
249
|
readonly externalFormReference?: string;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* The lifecycle-state-agnostic selectors for a user-task search: which process instance,
|
|
254
|
+
* assignee, and/or candidate group to match. Shared by {@link EngineClient.searchUserTasks}
|
|
255
|
+
* (which adds an optional `state`) and {@link EngineClient.openUserTasks} (which pins
|
|
256
|
+
* `state: "CREATED"` for you), so the two accessors cannot drift on how a task is selected.
|
|
257
|
+
*/
|
|
258
|
+
export interface UserTaskFilter {
|
|
259
|
+
processInstanceKey?: string;
|
|
260
|
+
assignee?: string;
|
|
261
|
+
candidateGroup?: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
252
264
|
/**
|
|
253
265
|
* A deployed form's form-js schema plus its identifying metadata, as
|
|
254
266
|
* {@link EngineClient.getForm} returns it. `schema` is the parsed form-js document
|
|
@@ -292,13 +304,28 @@ export interface EngineClient {
|
|
|
292
304
|
* By default this is unfiltered by lifecycle state and may return tasks in any state
|
|
293
305
|
* (e.g. completed/canceled); pass `state: "CREATED"` to constrain the search to open
|
|
294
306
|
* (answerable) tasks. Each result carries the task's resolved form linkage
|
|
295
|
-
* (`formKey`/`externalFormReference`) when present.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
307
|
+
* (`formKey`/`externalFormReference`) when present.
|
|
308
|
+
*
|
|
309
|
+
* **Footgun:** because this defaults to *any* lifecycle state and lags a completion,
|
|
310
|
+
* a just-answered task is still returned for a moment — so treating mere *existence*
|
|
311
|
+
* of a result as "open / actionable" projects a completed task as if a human could
|
|
312
|
+
* still act on it (a torn read-model row). {@link UserTaskSummary} deliberately omits
|
|
313
|
+
* `state`, so a caller cannot re-filter results defensively. If you are deciding
|
|
314
|
+
* whether a human can act — any reconcile / affordance path — use
|
|
315
|
+
* {@link EngineClient.openUserTasks} instead; reserve `searchUserTasks` for the rare
|
|
316
|
+
* legitimate "any lifecycle state" audit/read case. */
|
|
317
|
+
searchUserTasks(filter?: UserTaskFilter & {
|
|
300
318
|
state?: UserTaskState;
|
|
301
319
|
}): Promise<UserTaskSummary[]>;
|
|
320
|
+
/** The user tasks that are actually *open* (answerable) — semantically the accessor to
|
|
321
|
+
* reach for whenever you are deciding whether a human can act (reconcile / affordance
|
|
322
|
+
* paths). A thin, intent-named wrapper that pins the lifecycle-state invariant the SDK
|
|
323
|
+
* documents: exactly `searchUserTasks({ ...filter, state: "CREATED" })`, since `CREATED`
|
|
324
|
+
* is the only open state ({@link UserTaskState}). Unlike a bare `searchUserTasks`, it
|
|
325
|
+
* cannot surface a completed/canceled task as if it were still actionable — the footgun
|
|
326
|
+
* described on `searchUserTasks`. Same selectors as `searchUserTasks` minus `state`
|
|
327
|
+
* (pinned to `CREATED`). */
|
|
328
|
+
openUserTasks(filter?: UserTaskFilter): Promise<UserTaskSummary[]>;
|
|
302
329
|
/**
|
|
303
330
|
* Fetch a deployed form's form-js schema for the `taskInbox` surface. Resolve by
|
|
304
331
|
* `formKey` (the linkage the engine attaches to a user task) or, as a best-effort
|
|
@@ -29,6 +29,7 @@ const engine: EngineClient = {
|
|
|
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 () => [],
|
|
@@ -13,6 +13,7 @@ const fakeEngine: EngineClient = {
|
|
|
13
13
|
cancelInstance: async () => {},
|
|
14
14
|
publishMessage: async () => {},
|
|
15
15
|
searchUserTasks: async () => [],
|
|
16
|
+
openUserTasks: async () => [],
|
|
16
17
|
getForm: async () => null,
|
|
17
18
|
completeUserTask: async () => {},
|
|
18
19
|
searchProcessInstances: async () => [],
|
|
@@ -28,6 +28,7 @@ const engine: EngineClient = {
|
|
|
28
28
|
cancelInstance: async () => {},
|
|
29
29
|
publishMessage: async () => {},
|
|
30
30
|
searchUserTasks: async () => [],
|
|
31
|
+
openUserTasks: async () => [],
|
|
31
32
|
getForm: async () => null,
|
|
32
33
|
searchProcessInstances: async () => [],
|
|
33
34
|
completeUserTask: async () => {},
|
|
@@ -73,6 +73,7 @@ function cancelEngine(opts: {
|
|
|
73
73
|
createInstance: notUsed("createInstance"),
|
|
74
74
|
publishMessage: notUsed("publishMessage"),
|
|
75
75
|
searchUserTasks: notUsed("searchUserTasks"),
|
|
76
|
+
openUserTasks: notUsed("openUserTasks"),
|
|
76
77
|
getForm: notUsed("getForm"),
|
|
77
78
|
completeUserTask: notUsed("completeUserTask"),
|
|
78
79
|
registerWorker: notUsed("registerWorker"),
|
|
@@ -17,6 +17,7 @@ class MiniEngine implements EngineClient {
|
|
|
17
17
|
async cancelInstance(): Promise<void> {}
|
|
18
18
|
async publishMessage(): Promise<void> {}
|
|
19
19
|
async searchUserTasks(): Promise<[]> { return []; }
|
|
20
|
+
async openUserTasks(): Promise<[]> { return []; }
|
|
20
21
|
async getForm(): Promise<null> { return null; }
|
|
21
22
|
async completeUserTask(): Promise<void> {}
|
|
22
23
|
async searchProcessInstances(): Promise<[]> { return []; }
|