@nanobpm/urban 0.61.0 → 0.63.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.
@@ -1350,18 +1350,23 @@ test("child grids support per-row detail expansion, collapsed by default (#332)"
1350
1350
  // affordance instead of dumping the whole history flat.
1351
1351
  assert.match(js, /const cdetail = cg\.detail \|\| cg\.expandable \|\| null;/);
1352
1352
  assert.match(js, /const chasExpand = cdetail != null;/);
1353
- // The expander shares the top-level detail look: a pc-chevron button in a
1354
- // pc-row-actions cell, revealing detailPanel(cr, cdetail) in a following row.
1355
- assert.match(js, /class: "pc-btn pc-btn-sm pc-chevron" \}, collapsed \? "▸" : "▾"/);
1353
+ // The expander shares the top-level detail look and the shared chevronToggle()
1354
+ // factory: a pc-chevron disclosure button in a pc-row-actions cell, revealing
1355
+ // detailPanel(cr, cdetail) in a following row. setChevronOpen seeds the glyph +
1356
+ // aria-expanded from the persisted collapse state so they can't drift.
1357
+ // The panel's escalation form (if any) is threaded a child-scoped onSuccess so
1358
+ // answering it re-fetches only this child grid (load()), not the whole page (#333).
1359
+ assert.match(js, /const ctoggle = chevronToggle\("Toggle row details"\);\s*setChevronOpen\(ctoggle, !collapsed\);/);
1356
1360
  assert.match(js, /cells\.push\(el\("td", \{ class: "pc-row-actions" \}, ctoggle\)\);/);
1357
- assert.match(js, /cdtr\.firstChild\.append\(detailPanel\(cr, cdetail\)\)/);
1361
+ assert.match(js, /cdtr\.firstChild\.append\(detailPanel\(cr, cdetail, \(\) => load\(\)\)\)/);
1358
1362
  // Rows are collapsed by default; detail.defaultCollapsed/detail.collapsed (both
1359
1363
  // defaulting to true) seed the initial state.
1360
1364
  assert.match(js, /cdetail\.defaultCollapsed != null/);
1361
1365
  assert.match(js, /cdetail\.collapsed != null/);
1362
1366
  // detailPanel is now config-parametrised so the top-level grid and child grids
1363
- // share one implementation (link + fields + nested children + form).
1364
- assert.match(js, /function detailPanel\(row, cfg\)/);
1367
+ // share one implementation (link + fields + nested children + form). Its optional
1368
+ // onSuccess lets each caller scope the form's post-answer refresh (#333).
1369
+ assert.match(js, /function detailPanel\(row, cfg, onSuccess\)/);
1365
1370
  assert.match(js, /const d = cfg \|\| detail;/);
1366
1371
  });
1367
1372
 
@@ -1401,22 +1406,44 @@ test("child-grid detail panel never falls back to the top-level grid's form (#33
1401
1406
  const res = await dispatch("GET", "/app/runtime.js");
1402
1407
  const js = res.body ?? "";
1403
1408
  // detailPanel(cr, cdetail) renders a child grid's own detail config. Its form is
1404
- // detailForm(row, d.form): when the child config declares no `form` the panel
1405
- // must have NO form — it must NOT implicitly reuse the top-level grid's
1406
- // detail.form. So detailForm uses its argument verbatim rather than falling back
1407
- // to `detail && detail.form` when the argument is undefined.
1408
- assert.match(js, /function detailForm\(row, formCfg\) \{\s*const f = formCfg;/);
1409
- assert.doesNotMatch(js, /formCfg !== undefined \? formCfg : \(detail && detail\.form\)/);
1409
+ // built from d.form (the panel's own config): when the child config declares no
1410
+ // `form` the panel must have NO form — it must NOT implicitly reuse the top-level
1411
+ // grid's detail.form. So the shared buildDetailForm uses its `f` argument verbatim
1412
+ // (returning null when it is missing) rather than falling back to detail.form.
1413
+ assert.match(js, /const form = buildDetailForm\(d\.form, row,/);
1414
+ assert.match(js, /function buildDetailForm\(f, row, onSuccess\) \{\s*if \(!f \|\| !row\[f\.showWhenField\]\) return null;/);
1415
+ assert.doesNotMatch(js, /d\.form \|\| \(?detail/);
1416
+ });
1417
+
1418
+ test("child-grid and top-level detail forms thread different post-answer refreshes (#333)", async () => {
1419
+ const res = await dispatch("GET", "/app/runtime.js");
1420
+ const js = res.body ?? "";
1421
+ // The #333 vs #332 reconciliation (#336): both grids share one detailPanel /
1422
+ // buildDetailForm, but they thread DIFFERENT onSuccess callbacks. A child-grid
1423
+ // row's inline answer must refresh ONLY that child grid — via its local load()
1424
+ // re-fetch — so the answered row drops without a disruptive whole-page re-poll.
1425
+ // The child callback returns the load() promise (not swallowed) so a failed
1426
+ // re-fetch surfaces in buildDetailForm and re-enables its submit button.
1427
+ assert.match(js, /detailPanel\(cr, cdetail, \(\) => load\(\)\)/);
1428
+ // load() is the child-scoped re-fetch: it repaints just this child grid's tbody.
1429
+ assert.match(js, /async function load\(\) \{[\s\S]*?cbody\.replaceChildren\(\);/);
1430
+ // The top-level grid keeps its whole-page re-poll: detailPanel's default onSuccess
1431
+ // (used when no child-scoped callback is passed) dispatches pc:refresh.
1432
+ assert.match(js, /onSuccess \|\| \(\(\) => \{ document\.dispatchEvent\(new CustomEvent\("pc:refresh"\)\); \}\)/);
1433
+ // The single shared expander (cdetail) hosts the form — there is no duplicate,
1434
+ // parallel form-only child expander (the pre-reconciliation cdetailForm surface).
1435
+ assert.doesNotMatch(js, /const cdetailForm = /);
1410
1436
  });
1411
1437
 
1412
1438
  test("child-grid fetch failure renders a full-width error row (#332)", async () => {
1413
1439
  const res = await dispatch("GET", "/app/runtime.js");
1414
1440
  const js = res.body ?? "";
1415
- // The child grid's colspan is computed eagerly, BEFORE the getJSON() fetch, so
1416
- // the catch path's error row (colspan: cspan) spans the whole table even when
1441
+ // The child grid's colspan is computed eagerly, BEFORE the getJSON() fetch (now
1442
+ // wrapped in load() so the grid can re-fetch itself after an inline answer, #333),
1443
+ // so the catch path's error row (colspan: cspan) spans the whole table even when
1417
1444
  // the fetch throws. A lazily-assigned cspan would be undefined in the catch.
1418
- assert.match(js, /const cspan = String\(\(ccols\.length \|\| 1\) \+ cextra\);\s*try \{/);
1419
- assert.match(js, /\} catch \(e\) \{\s*cbody\.append\(el\("tr", \{\}, el\("td", \{ colspan: cspan \}/);
1445
+ assert.match(js, /const cspan = String\(\(ccols\.length \|\| 1\) \+ cextra\);[\s\S]*?async function load\(\) \{[\s\S]*?await getJSON\(dataUrl\(cg\.source/);
1446
+ assert.match(js, /\} catch \(e\) \{\s*cbody\.replaceChildren\(el\("tr", \{\}, el\("td", \{ colspan: cspan \}/);
1420
1447
  });
1421
1448
 
1422
1449
  test("the renderer honours an optional Tier-2 page-level mobile layout variant (#268)", async () => {