diff --git a/tables/js/api-actions.js b/tables/js/api-actions.js index a002983..56c624d 100644 --- a/tables/js/api-actions.js +++ b/tables/js/api-actions.js @@ -49,9 +49,10 @@ var inputs = {}; (c.fields || []).forEach(function (f) { var lab = el('label', { class: 'api-modal__field' }); - lab.appendChild(el('span', null, f.label || f.name)); + lab.appendChild(el('span', null, (f.label || f.name) + (f.required ? ' *' : ''))); var inp = el('input', { type: f.type || 'text' }); if (f.placeholder) inp.setAttribute('placeholder', f.placeholder); + if (f.required) inp.required = true; inputs[f.name] = inp; lab.appendChild(inp); form.appendChild(lab); @@ -76,6 +77,12 @@ form.addEventListener('submit', function (e) { e.preventDefault(); err.hidden = true; + var missing = (c.fields || []).filter(function (f) { return f.required && !inputs[f.name].value.trim(); }); + if (missing.length) { + err.textContent = 'Required: ' + missing.map(function (f) { return f.label || f.name; }).join(', '); + err.hidden = false; + return; + } var body = {}; (c.fields || []).forEach(function (f) { var v = inputs[f.name].value.trim(); @@ -83,6 +90,9 @@ // Date fields → RFC3339 so the Go time.Time decoder accepts them. body[f.name] = (f.type === 'date') ? new Date(v + 'T00:00:00').toISOString() : v; }); + // Constant fields the server requires but the user doesn't set + // (e.g. project create's parent="/"). + if (c.fixed) Object.keys(c.fixed).forEach(function (k) { body[k] = c.fixed[k]; }); submit.disabled = true; fetch(c.url, { method: 'POST', @@ -175,12 +185,40 @@ }); } + // Per-row navigation: clicking a row opens its data-url (the project / + // subtree it represents) — used by the profile "Effective access" table. + // Clicks on inner controls (buttons/links/inputs) are left alone. + function ensureRowNav() { + var tbody = document.querySelector('#table-root tbody'); + if (!tbody) return; + var trs = tbody.querySelectorAll('tr'); + for (var i = 0; i < trs.length; i++) { + var tr = trs[i]; + if (tr.getAttribute('data-nav') === '1') continue; + var url = tr.getAttribute('data-url'); + if (!url) continue; + tr.setAttribute('data-nav', '1'); + tr.style.cursor = 'pointer'; + (function (target) { + // Capture phase: fire before the tables editor's per-cell + // click handlers (which would otherwise swallow the click on + // read-only rows). Inner controls (buttons/links/inputs) still + // opt out. + tr.addEventListener('click', function (e) { + if (e.target.closest('button, a, input')) return; + window.location.href = target; + }, true); + })(url); + } + } + function tick() { var c = cfg(); if (!c) return; hideNative(); if (c.create) mountCreate(c.create); if (c.deleteRow) ensureRowDelete(c.deleteRow); + if (c.rowNav) ensureRowNav(); } function start() { diff --git a/zddc/internal/handler/profilehandler_test.go b/zddc/internal/handler/profilehandler_test.go index 8e89d58..249dcca 100644 --- a/zddc/internal/handler/profilehandler_test.go +++ b/zddc/internal/handler/profilehandler_test.go @@ -235,32 +235,6 @@ func TestServeProfileLogsLevelFilter(t *testing.T) { } } -// stripTemplates removes every block from the -// HTML body so substring assertions check only ACTIVE markup — i.e. live -// DOM content the user (and their browser) actually sees, as opposed to -// inert content that JS may clone in based on a later access fetch. -// -// Naive but sufficient for the controlled output of profileTemplate (the -// template tags are unnested and well-formed). If the page ever grows -// nested templates, swap this for an html.Tokenizer-based pass. -func stripTemplates(body string) string { - var b strings.Builder - for { - i := strings.Index(body, "") - if j < 0 { - // Unterminated "):] - } -} - // TestServeProfileHTMLLayered pins the page-render contract after the // lazy-load refactor: // @@ -306,85 +280,53 @@ func TestServeProfileHTMLLayered(t *testing.T) { return rec.Body.String() } - // Anonymous: identity says "Not signed in", no live admin markup, no - // diagnostics. The