@nanobpm/urban 0.63.0 → 0.64.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.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",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"dependencies": {
|
|
102
102
|
"@nanobpm/nano-app-schema": "^0.9.0",
|
|
103
103
|
"@nanobpm/nano-sdk": "^1.2.5",
|
|
104
|
-
"@nanobpm/workflow": "^0.
|
|
104
|
+
"@nanobpm/workflow": "^0.9.0",
|
|
105
105
|
"bpmn-auto-layout": "^2.0.0-alpha.2",
|
|
106
106
|
"create-urban-app": "^0.14.0",
|
|
107
107
|
"yaml": "^2.9.0"
|
|
@@ -633,6 +633,39 @@ function pageHashHref(page, key) {
|
|
|
633
633
|
return "#/" + encodeURIComponent(page) + "/" + encodeURIComponent(keyStr);
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
+
// SINGLE source of truth for a console process-explorer link. The grid link cell
|
|
637
|
+
// (link kind "processExplorer") and the pipeline locus link (issue #347) both
|
|
638
|
+
// route here so they can't drift on the href or the embedded-vs-standalone
|
|
639
|
+
// navigation. The console path is constructed HERE (never taken from row data)
|
|
640
|
+
// and the trimmed key URL-encoded, so row data can't smuggle a path/scheme.
|
|
641
|
+
// Returns null for a blank key (after trimming) so the caller keeps the node
|
|
642
|
+
// plain. Standalone the anchor opens a new tab (hardened rel); embedded in the
|
|
643
|
+
// console a plain primary click instead routes the host's explorer in place via
|
|
644
|
+
// the nano-navigate bridge (a non-primary/modified click keeps the new-tab
|
|
645
|
+
// behavior).
|
|
646
|
+
/** @param {any} label
|
|
647
|
+
* @param {any} key
|
|
648
|
+
* @param {string} cls
|
|
649
|
+
* @returns {any}
|
|
650
|
+
*/
|
|
651
|
+
function explorerAnchor(label, key, cls) {
|
|
652
|
+
const keyStr = key == null ? "" : String(key).trim();
|
|
653
|
+
if (keyStr === "") return null;
|
|
654
|
+
const href = "/console/explorer?instance=" + encodeURIComponent(keyStr);
|
|
655
|
+
return el("a", {
|
|
656
|
+
class: cls,
|
|
657
|
+
href,
|
|
658
|
+
target: "_blank",
|
|
659
|
+
rel: "noopener noreferrer",
|
|
660
|
+
/** @param {MouseEvent} ev */
|
|
661
|
+
onclick: (ev) => {
|
|
662
|
+
if (ev.button !== 0) return;
|
|
663
|
+
if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
|
|
664
|
+
if (hostNavigate("processExplorer", { instance: keyStr })) ev.preventDefault();
|
|
665
|
+
},
|
|
666
|
+
}, label);
|
|
667
|
+
}
|
|
668
|
+
|
|
636
669
|
// Coerce a not-in-path value (issue #265) into a Set of stage keys. Accepts an
|
|
637
670
|
// array of keys or a comma/whitespace-separated string; null/undefined → empty
|
|
638
671
|
// set. Used to mark the stages a given row skips (dashed/omitted).
|
|
@@ -746,27 +779,13 @@ function gridCell(col, row, role) {
|
|
|
746
779
|
}
|
|
747
780
|
}
|
|
748
781
|
if (primary === text && col.link && col.link.kind === "processExplorer" && col.link.keyField) {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
target: "_blank",
|
|
757
|
-
rel: "noopener noreferrer",
|
|
758
|
-
// Embedded in the console a plain primary click routes the host's
|
|
759
|
-
// explorer in place (via the nano-navigate bridge) instead of opening
|
|
760
|
-
// a new window. A non-primary button (middle/right = new-tab/context
|
|
761
|
-
// intent), a modified click (⌘/ctrl/shift/alt = new-tab intent) and
|
|
762
|
-
// any standalone run fall through to the native anchor above.
|
|
763
|
-
/** @param {MouseEvent} ev */
|
|
764
|
-
onclick: (ev) => {
|
|
765
|
-
if (ev.button !== 0) return;
|
|
766
|
-
if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
|
|
767
|
-
if (hostNavigate("processExplorer", { instance: keyStr })) ev.preventDefault();
|
|
768
|
-
},
|
|
769
|
-
}, text);
|
|
782
|
+
// Route through the shared explorerAnchor builder (single source of truth) so
|
|
783
|
+
// this cell and the pipeline locus link can't drift on the href or the
|
|
784
|
+
// embedded-in-place / standalone-new-tab navigation. A blank key → null →
|
|
785
|
+
// the cell stays plain text.
|
|
786
|
+
const a = explorerAnchor(text, row[col.link.keyField], "pc-link");
|
|
787
|
+
if (text !== "" && a) {
|
|
788
|
+
primary = a;
|
|
770
789
|
}
|
|
771
790
|
}
|
|
772
791
|
return cellTd(primary, text, subText, truncate, mob);
|
|
@@ -924,13 +943,28 @@ function pipelineCell(col, row, text, subText, truncate, tdAttrs) {
|
|
|
924
943
|
const badgeText = col.badgeField != null && row[col.badgeField] != null ? String(row[col.badgeField]) : "";
|
|
925
944
|
const badgeTone = state === "failed" || state === "blocked" ? "danger" : "warn";
|
|
926
945
|
// Optional locus out-link: the relevant stage (default the active one, or
|
|
927
|
-
// locus.stage by key) becomes
|
|
928
|
-
//
|
|
929
|
-
//
|
|
946
|
+
// locus.stage by key) becomes a link when the row's locus key is non-empty.
|
|
947
|
+
// Two link kinds are supported, mirroring the grid link cell and built via the
|
|
948
|
+
// SAME single-source builders so the two can't drift: `page` → an in-app
|
|
949
|
+
// #/<page>/<key> hash route (pageHashHref, safePageId guard + encodeURIComponent);
|
|
950
|
+
// `processExplorer` → the console explorer for the row's process instance
|
|
951
|
+
// (explorerAnchor, embedded-in-place / standalone-new-tab). An unknown kind or
|
|
952
|
+
// blank key yields no link, so the stage stays plain.
|
|
930
953
|
const locus = col.locus;
|
|
931
954
|
const locusKey = locus && locus.field != null && row[locus.field] != null ? String(row[locus.field]) : "";
|
|
932
|
-
|
|
933
|
-
|
|
955
|
+
/** @param {any} label
|
|
956
|
+
* @returns {any} an anchor node for the locus stage, or null (→ plain label). */
|
|
957
|
+
const locusLabelNode = (label) => {
|
|
958
|
+
if (!locus || !locus.link || locusKey === "") return null;
|
|
959
|
+
if (locus.link.kind === "page" && locus.link.page) {
|
|
960
|
+
const href = pageHashHref(locus.link.page, locusKey);
|
|
961
|
+
return href === "" ? null : el("a", { class: "pc-link pc-pipe-label", href }, label);
|
|
962
|
+
}
|
|
963
|
+
if (locus.link.kind === "processExplorer") {
|
|
964
|
+
return explorerAnchor(label, locusKey, "pc-link pc-pipe-label");
|
|
965
|
+
}
|
|
966
|
+
return null;
|
|
967
|
+
};
|
|
934
968
|
const track = el("div", {
|
|
935
969
|
class: "pc-pipe",
|
|
936
970
|
role: "list",
|
|
@@ -968,11 +1002,10 @@ function pipelineCell(col, row, text, subText, truncate, tdAttrs) {
|
|
|
968
1002
|
} else {
|
|
969
1003
|
cls = "pc-pipe-upcoming"; word = "upcoming";
|
|
970
1004
|
}
|
|
971
|
-
const
|
|
1005
|
+
const atLocus = locus != null && (locus.stage != null ? String(s.key) === String(locus.stage) : i === activeIdx);
|
|
972
1006
|
const label = labelOf(s);
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
: el("span", { class: "pc-pipe-label" }, label);
|
|
1007
|
+
const locusNode = atLocus ? locusLabelNode(label) : null;
|
|
1008
|
+
const labelNode = locusNode != null ? locusNode : el("span", { class: "pc-pipe-label" }, label);
|
|
976
1009
|
/** @type {Record<string, any>} */
|
|
977
1010
|
const attrs = { class: "pc-pipe-stage " + cls, role: "listitem", "aria-label": label + " \u2014 " + word };
|
|
978
1011
|
if (current) attrs["aria-current"] = "step";
|
|
@@ -1561,7 +1561,26 @@ test("pipeline builds a locus link via the shared pageHashHref route builder, on
|
|
|
1561
1561
|
assert.match(js, /const href = pageHashHref\(col\.link\.page, row\[col\.link\.keyField\]\)/);
|
|
1562
1562
|
// The locus link uses it and the shared .pc-link class; blank key → "" → plain.
|
|
1563
1563
|
assert.match(js, /pageHashHref\(locus\.link\.page, locusKey\)/);
|
|
1564
|
-
assert.match(js, /el\("a", \{ class: "pc-link pc-pipe-label", href
|
|
1564
|
+
assert.match(js, /el\("a", \{ class: "pc-link pc-pipe-label", href \}, label\)/);
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
test("pipeline locus supports a processExplorer link routing to the console explorer via the shared explorerAnchor", async () => {
|
|
1568
|
+
const res = await dispatch("GET", "/app/runtime.js");
|
|
1569
|
+
const js = res.body ?? "";
|
|
1570
|
+
// A pipeline column whose locus declares `link:{ kind:"processExplorer" }` links
|
|
1571
|
+
// the locus stage to the console explorer for the row's process instance — via
|
|
1572
|
+
// the SAME single-source explorerAnchor builder the grid link cell uses, so the
|
|
1573
|
+
// href and the embedded-in-place / standalone-new-tab navigation can't drift.
|
|
1574
|
+
assert.match(js, /function explorerAnchor\(label, key, cls\)/);
|
|
1575
|
+
assert.match(js, /"\/console\/explorer\?instance="\s*\+\s*encodeURIComponent\(keyStr\)/);
|
|
1576
|
+
assert.match(js, /hostNavigate\("processExplorer",\s*\{\s*instance:\s*keyStr\s*\}\)/);
|
|
1577
|
+
// The pipeline locus branches to it on the processExplorer kind, reusing the
|
|
1578
|
+
// .pc-pipe-label class; the grid link cell routes through the same builder.
|
|
1579
|
+
assert.match(js, /locus\.link\.kind === "processExplorer"/);
|
|
1580
|
+
assert.match(js, /return explorerAnchor\(label, locusKey, "pc-link pc-pipe-label"\)/);
|
|
1581
|
+
assert.match(js, /explorerAnchor\(text, row\[col\.link\.keyField\], "pc-link"\)/);
|
|
1582
|
+
// Unknown kind / blank key → no link → the stage stays plain (graceful).
|
|
1583
|
+
assert.match(js, /if \(!locus \|\| !locus\.link \|\| locusKey === ""\) return null/);
|
|
1565
1584
|
});
|
|
1566
1585
|
|
|
1567
1586
|
test("pipeline exposes step semantics to assistive tech (role/list + aria-current)", async () => {
|