@nanobpm/urban 0.19.0 → 0.20.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.
@@ -392,6 +392,27 @@ function renderText(node) {
392
392
  return el(v === "heading" ? "h1" : "p", { class: cls }, node.props.text || "");
393
393
  }
394
394
 
395
+ // A grid td cell. When the column declares linkField, the cell text (the
396
+ // column's own value) becomes a link to the URL held in that other field,
397
+ // opened in a new tab. Only http(s) hrefs are linked — anything else (e.g. a
398
+ // javascript: URL smuggled through row data) falls back to plain text — and
399
+ // external links get rel=noopener noreferrer so the opened page can't reach
400
+ // window.opener. Shared by the top-level grid and child grids.
401
+ function gridCell(col, row) {
402
+ const text = row[col.field] == null ? "" : String(row[col.field]);
403
+ if (col.linkField) {
404
+ const href = row[col.linkField] == null ? "" : String(row[col.linkField]);
405
+ if (text !== "" && /^https?:\/\//i.test(href)) {
406
+ return el(
407
+ "td",
408
+ {},
409
+ el("a", { class: "pc-link", href, target: "_blank", rel: "noopener noreferrer" }, text),
410
+ );
411
+ }
412
+ }
413
+ return el("td", {}, text);
414
+ }
415
+
395
416
  function renderActionForm(node) {
396
417
  const p = node.props;
397
418
  const card = el("section", { class: "pc-card" });
@@ -552,7 +573,7 @@ function renderDataGrid(node) {
552
573
  cbody.append(el("tr", {}, el("td", { colspan: cspan }, "None")));
553
574
  }
554
575
  for (const cr of rows) {
555
- const cells = ccols.map((c) => el("td", {}, cr[c.field] == null ? "" : String(cr[c.field])));
576
+ const cells = ccols.map((c) => gridCell(c, cr));
556
577
  if (cg.lazyField) {
557
578
  const lf = cg.lazyField;
558
579
  const has = cr[lf.field] != null && String(cr[lf.field]).trim() !== "";
@@ -606,7 +627,7 @@ function renderDataGrid(node) {
606
627
  }
607
628
 
608
629
  function renderRow(row) {
609
- const cells = cols.map((c) => el("td", {}, row[c.field] == null ? "" : String(row[c.field])));
630
+ const cells = cols.map((c) => gridCell(c, row));
610
631
  let toggle = null;
611
632
  if (hasExtra) {
612
633
  const actionCell = el("td", { class: "pc-row-actions" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.19.0",
3
+ "version": "0.20.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",
@@ -89,6 +89,19 @@ test("GET /app/runtime.js serves the renderer module", async () => {
89
89
  assert.match(res.body ?? "", /pc:refresh/);
90
90
  });
91
91
 
92
+ test("renderer wires a column's linkField to a new-tab anchor", async () => {
93
+ const res = await dispatch("GET", "/app/runtime.js");
94
+ const js = res.body ?? "";
95
+ // A column declaring `linkField` renders its text as a link to that field's
96
+ // URL, opened safely in a new tab. Guard the whole shape so the feature can't
97
+ // silently regress: the field is read, http(s) is required, and the anchor
98
+ // opens in a new tab with a hardened rel.
99
+ assert.match(js, /col\.linkField/);
100
+ assert.match(js, /\^https\?:/);
101
+ assert.match(js, /target: "_blank"/);
102
+ assert.match(js, /rel: "noopener noreferrer"/);
103
+ });
104
+
92
105
  test("GET /app/pages/<id> returns the page json, 404 for unknown", async () => {
93
106
  const ok = await dispatch("GET", "/app/pages/home");
94
107
  assert.equal(ok.status, 200);
@@ -427,6 +427,27 @@ function renderText(node) {
427
427
  return el(v === "heading" ? "h1" : "p", { class: cls }, node.props.text || "");
428
428
  }
429
429
 
430
+ // A grid td cell. When the column declares linkField, the cell text (the
431
+ // column's own value) becomes a link to the URL held in that other field,
432
+ // opened in a new tab. Only http(s) hrefs are linked — anything else (e.g. a
433
+ // javascript: URL smuggled through row data) falls back to plain text — and
434
+ // external links get rel=noopener noreferrer so the opened page can't reach
435
+ // window.opener. Shared by the top-level grid and child grids.
436
+ function gridCell(col, row) {
437
+ const text = row[col.field] == null ? "" : String(row[col.field]);
438
+ if (col.linkField) {
439
+ const href = row[col.linkField] == null ? "" : String(row[col.linkField]);
440
+ if (text !== "" && /^https?:\/\//i.test(href)) {
441
+ return el(
442
+ "td",
443
+ {},
444
+ el("a", { class: "pc-link", href, target: "_blank", rel: "noopener noreferrer" }, text),
445
+ );
446
+ }
447
+ }
448
+ return el("td", {}, text);
449
+ }
450
+
430
451
  function renderActionForm(node) {
431
452
  const p = node.props;
432
453
  const card = el("section", { class: "pc-card" });
@@ -587,7 +608,7 @@ function renderDataGrid(node) {
587
608
  cbody.append(el("tr", {}, el("td", { colspan: cspan }, "None")));
588
609
  }
589
610
  for (const cr of rows) {
590
- const cells = ccols.map((c) => el("td", {}, cr[c.field] == null ? "" : String(cr[c.field])));
611
+ const cells = ccols.map((c) => gridCell(c, cr));
591
612
  if (cg.lazyField) {
592
613
  const lf = cg.lazyField;
593
614
  const has = cr[lf.field] != null && String(cr[lf.field]).trim() !== "";
@@ -641,7 +662,7 @@ function renderDataGrid(node) {
641
662
  }
642
663
 
643
664
  function renderRow(row) {
644
- const cells = cols.map((c) => el("td", {}, row[c.field] == null ? "" : String(row[c.field])));
665
+ const cells = cols.map((c) => gridCell(c, row));
645
666
  let toggle = null;
646
667
  if (hasExtra) {
647
668
  const actionCell = el("td", { class: "pc-row-actions" });