Files
plainpages/views/partials/data-table.ejs
lilleman bd20d00714 §8 review checkpoint (todo §8); ran the architecture + product reviewers on the whole project and addressed findings. Critical (arch): "Testing & CI" shipped no CI automation — added scripts/ci.sh, the whole gate in one command (pin-lockstep check → typecheck → units (count guard) → the 4 E2E suites, each on its own named fresh stack with guaranteed down -v + non-zero exit on first failure). The gate immediately caught a latent bug: the auth-refresh suite booted Hydra (inherited §6 web→hydra dep) but the e2e overlays don't run Hydra with --dev, so it never went healthy — dropped Hydra from the auth suite's web deps (it never needed it). Product 🔴: the README Status note claimed auth/Hydra were unbuilt (false after §4/§6/§8) — corrected it + dropped the now-false _(planned)_ markers on the Auth/MVP sections. Product 🟡: added a login-only "Forgot password?" link (the recovery flow was unreachable from /login) and a data-table empty-state row (blank list tables, recurring deferral) — both tests-first. Docs: README Layout e2e line + e2e/package.json updated for the §8 suites. Stability-reviewer APPROVE-with-nits; addressed both (per-suite compose project names; grep || true) and fixed a project-name dot bug it introduced. Corrected a reviewer error (bootstrap uses restart on-failure:5, not unless-stopped). typecheck + 306 units green; scripts/ci.sh green end-to-end (visual 9 · auth 1 · oauth 2 · full 6), all stacks torn down. Deferred to §9: the app.ts internal route-table (raised urgency), visual-parity for admin/consent screens, a key-rotation E2E; L3 (plugin-api barrel in shifts.test) → the §8 test-cleanup item.
2026-06-19 20:08:48 +02:00

79 lines
4.3 KiB
Plaintext

<%#
Data table: sortable headers, row-select, typed cells, badges, kebab row actions.
Mirrors the html-css-foundation markup; zero-JS (sort = links, select highlight = CSS).
Config:
caption?, selectable?, actions? sr-only caption; toggle the check / kebab columns
columns: { label, sortable?, sort?: "asc"|"desc", href?, className? }[]
rows: { name?, cells: Cell[], actions?: Action[] }[]
Cell ∈ string | { text, className? } | { user:{name,initials} } | { rowHeader:{text,href?} } | { badge:{tone,label} } | { html, className? }
user + rowHeader cells render as <th scope="row"> — they identify the row (the row header).
Action = { label, icon?, href?, danger?, separatorBefore? }
%><%
const caption = locals.caption;
const selectable = !!locals.selectable;
const withActions = !!locals.actions;
const columns = locals.columns || [];
const rows = locals.rows || [];
const emptyText = locals.emptyText || "Nothing here yet."; // shown when a table that has columns has no rows
-%>
<div class="table-wrap">
<table class="table">
<% if (caption) { -%>
<caption class="sr-only"><%= caption %></caption>
<% } -%>
<thead>
<tr>
<% if (selectable) { -%>
<th class="col-check" scope="col"><input type="checkbox" aria-label="Select all rows"></th>
<% } -%>
<% columns.forEach((col) => { -%>
<% if (col.sortable) { -%>
<th scope="col"<% if (col.sort === "asc") { %> aria-sort="ascending"<% } else if (col.sort === "desc") { %> aria-sort="descending"<% } %><% if (col.className) { %> class="<%= col.className %>"<% } %>><a class="th-sort" href="<%= col.href %>"><%= col.label %> <svg class="ico ico-sm sort-ico"><use href="#<%= col.sort ? "i-up" : "i-sort" %>"/></svg></a></th>
<% } else { -%>
<th scope="col"<% if (col.className) { %> class="<%= col.className %>"<% } %>><%= col.label %></th>
<% } -%>
<% }) -%>
<% if (withActions) { -%>
<th class="col-actions" scope="col"><span class="sr-only">Actions</span></th>
<% } -%>
</tr>
</thead>
<tbody>
<% if (rows.length === 0 && columns.length) { -%>
<tr><td class="table-empty" colspan="<%= columns.length + (selectable ? 1 : 0) + (withActions ? 1 : 0) %>"><%= emptyText %></td></tr>
<% } -%>
<% rows.forEach((row) => { -%>
<tr>
<% if (selectable) { -%>
<td class="col-check"><input type="checkbox" class="row-select" aria-label="Select <%= row.name || "row" %>"></td>
<% } -%>
<% (row.cells || []).forEach((cell) => { -%>
<% if (typeof cell === "string") { -%>
<td><%= cell %></td>
<% } else if (cell.user) { -%>
<th scope="row"><span class="cell-user"><span class="avatar" aria-hidden="true"><%= cell.user.initials %></span><span class="cell-strong"><%= cell.user.name %></span></span></th>
<% } else if (cell.rowHeader) { -%>
<th scope="row"><% if (cell.rowHeader.href) { %><a class="cell-strong" href="<%= cell.rowHeader.href %>"><%= cell.rowHeader.text %></a><% } else { %><span class="cell-strong"><%= cell.rowHeader.text %></span><% } %></th>
<% } else if (cell.badge) { -%>
<td><span class="badge <%= cell.badge.tone %>"><span class="dot"></span><%= cell.badge.label %></span></td>
<% } else if (cell.html != null) { -%>
<td<% if (cell.className) { %> class="<%= cell.className %>"<% } %>><%- cell.html %></td>
<% } else { -%>
<td<% if (cell.className) { %> class="<%= cell.className %>"<% } %>><%= cell.text %></td>
<% } -%>
<% }) -%>
<% if (withActions) { -%>
<% if ((row.actions || []).length) { -%>
<td class="col-actions"><details class="menu kebab"><summary aria-label="Row actions for <%= row.name || "row" %>"><svg class="ico ico-sm"><use href="#i-kebab"/></svg></summary><div class="menu-pop"><% row.actions.forEach((a) => { -%>
<% if (a.separatorBefore) { %><div class="menu-sep"></div><% } %><% if (a.href) { %><a class="menu-item<% if (a.danger) { %> danger<% } %>" href="<%= a.href %>"><% if (a.icon) { %><svg class="ico"><use href="#<%= a.icon %>"/></svg><% } %><%= a.label %></a><% } else { %><button class="menu-item<% if (a.danger) { %> danger<% } %>" type="button"><% if (a.icon) { %><svg class="ico"><use href="#<%= a.icon %>"/></svg><% } %><%= a.label %></button><% } %><% }) -%>
</div></details></td>
<% } else { -%>
<td class="col-actions"></td>
<% } -%>
<% } -%>
</tr>
<% }) -%>
</tbody>
</table>
</div>