Three triggers for flushing pending edits:
- Save button in the toolbar — shown only when ≥1 row is dirty,
label reads "Save (N unsaved)". Disappears after a clean settle.
- Ctrl+S (Cmd+S) anywhere on the page, capturing-phase so it beats
the browser's "Save Page As" default.
- focusout of #table-root with a relatedTarget outside the grid —
catches "edit cell, click a header link, expect it to save".
The row-blur trigger stays — moving between rows still flushes. The
new triggers fill the gap when the user edits one row and then leaves
the grid entirely without first navigating to another row.
Dirty marker gets a 4px (was 3px) left swatch AND a faint blue
background tint on the row, so "unsaved" reads as a row state rather
than a small marker on the edge.
editor.setDraft / clearDraftField notify save.onDraftsChanged,
which refreshes the Save button + reapplies the dirty class.
saveRow on 200/201/202 also refreshes the button so it disappears
the moment its row settles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
196 lines
9.4 KiB
HTML
196 lines
9.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>ZDDC Table</title>
|
|
<link rel="icon" type="image/svg+xml" href="{{FAVICON}}">
|
|
<style>
|
|
{{CSS_PLACEHOLDER}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header class="app-header">
|
|
<div class="header-left">
|
|
<svg class="app-header__logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" aria-hidden="true">
|
|
<rect width="64" height="64" rx="12" fill="#1e3a5f"/>
|
|
<g fill="#fff">
|
|
<rect x="14" y="18" width="36" height="7"/>
|
|
<polygon points="43,25 50,25 21,43 14,43"/>
|
|
<rect x="14" y="43" width="36" height="7"/>
|
|
</g>
|
|
</svg>
|
|
<div class="header-title-group">
|
|
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
|
<span class="build-timestamp">{{BUILD_LABEL}}</span>
|
|
</div>
|
|
</div>
|
|
<div class="header-right">
|
|
<!-- Elevation toggle slot. shared/elevation.js fills it
|
|
when /.profile/access reports the user has admin
|
|
authority; stays empty + hidden for non-admins so
|
|
the chrome is quiet for the common case. -->
|
|
<span id="elevation-toggle" class="elevation-toggle hidden"
|
|
title="Opt into your admin powers for the next 30 minutes (sudo-style)."></span>
|
|
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)">◐</button>
|
|
<button id="help-btn" class="btn btn-secondary" title="Help" aria-label="Help">?</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Table mode: shown for /<dir>/table.html requests. -->
|
|
<main id="table-mode" class="table-main" hidden>
|
|
<div id="table-description" class="table-description" hidden></div>
|
|
<div id="table-status" class="table-status" hidden></div>
|
|
<div class="table-toolbar" id="table-toolbar">
|
|
<div class="table-toolbar__left">
|
|
<span id="table-rowcount" class="table-rowcount" aria-live="polite"></span>
|
|
<button type="button" id="table-clear-filters" class="btn btn-secondary btn-sm" hidden>Clear filters</button>
|
|
</div>
|
|
<div class="table-toolbar__right">
|
|
<button type="button" id="table-save" class="btn btn-primary btn-sm" hidden>Save</button>
|
|
<button type="button" id="table-export-csv" class="btn btn-secondary btn-sm" hidden>Export CSV</button>
|
|
<a id="table-add-row" class="btn btn-primary btn-sm" hidden>+ Add row</a>
|
|
</div>
|
|
</div>
|
|
<div class="table-scroll">
|
|
<table id="table-root" class="zddc-table" aria-describedby="table-description">
|
|
<thead></thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
<div id="table-empty" class="table-empty" hidden>No rows match the current filters.</div>
|
|
</main>
|
|
|
|
<!-- Form mode: shown for /<dir>/form.html and /<dir>/<id>.yaml.html
|
|
requests. Same bundle ships both modes so a row's "+ Add row"
|
|
and click-to-edit reuse the table tool's spec, validator, and
|
|
file-IO instead of duplicating them in a separate form HTML. -->
|
|
<main id="form-mode" class="form-main" hidden>
|
|
<div id="form-status" class="form-status" hidden></div>
|
|
<form id="form-root" class="form-root" novalidate></form>
|
|
<div class="form-actions">
|
|
<button type="button" id="submit-btn" class="btn btn-primary">Submit</button>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Help Panel -->
|
|
<aside id="help-panel" class="help-panel" hidden aria-labelledby="help-panel-title">
|
|
<div class="help-panel__header">
|
|
<h2 id="help-panel-title" class="help-panel__title">Help — ZDDC Table</h2>
|
|
<button type="button" class="help-panel__close" id="help-panel-close" aria-label="Close">×</button>
|
|
</div>
|
|
<div class="help-panel__body">
|
|
<h3>What is this table?</h3>
|
|
<p>The directory you opened — say <code>archive/Acme/mdl/</code> —
|
|
<em>is</em> the table. <code>table.yaml</code> describes the
|
|
columns; <code>form.yaml</code> describes the row-edit form
|
|
schema; every other <code>.yaml</code> file in the directory
|
|
is one row. Copying the directory anywhere takes the whole
|
|
table (spec + form + every row) with it.</p>
|
|
|
|
<h3>Editing cells</h3>
|
|
<p>Click a cell to select it. Then:</p>
|
|
<dl>
|
|
<dt><kbd>↑</kbd> / <kbd>↓</kbd> / <kbd>←</kbd> / <kbd>→</kbd></dt>
|
|
<dd>Move selection. Hold <kbd>Shift</kbd> to extend a range.</dd>
|
|
<dt><kbd>Tab</kbd> / <kbd>Shift+Tab</kbd></dt>
|
|
<dd>Move right / left, wrap to next / previous row.</dd>
|
|
<dt><kbd>Enter</kbd> / <kbd>F2</kbd> / double-click / typing</dt>
|
|
<dd>Enter edit mode. Typing replaces the cell value; the
|
|
others keep it.</dd>
|
|
<dt><kbd>Enter</kbd> in edit mode</dt>
|
|
<dd>Commit and move down.</dd>
|
|
<dt><kbd>Tab</kbd> in edit mode</dt>
|
|
<dd>Commit and move right.</dd>
|
|
<dt><kbd>Esc</kbd></dt>
|
|
<dd>Cancel the edit; restore the prior value.</dd>
|
|
<dt><kbd>Delete</kbd> / <kbd>Backspace</kbd></dt>
|
|
<dd>Clear every cell in the current selection.</dd>
|
|
<dt><kbd>Ctrl+D</kbd> / <kbd>Ctrl+R</kbd></dt>
|
|
<dd>Fill the top row down / left column right through the
|
|
selected range.</dd>
|
|
<dt><kbd>Ctrl+C</kbd> / <kbd>Ctrl+V</kbd></dt>
|
|
<dd>Copy / paste — interoperates with Excel and Google
|
|
Sheets via tab-separated values.</dd>
|
|
<dt><kbd>Ctrl+Z</kbd></dt>
|
|
<dd>Undo the last edit (one history per session).</dd>
|
|
</dl>
|
|
<p>Edits save automatically when you move to a different row.
|
|
A small left-edge swatch on the row indicates state:
|
|
<strong>blue</strong> = unsaved, <strong>amber</strong> = the
|
|
server flagged a validation error, <strong>orange</strong> =
|
|
someone else changed this row since you loaded it (you'll
|
|
get a prompt with <em>Use mine</em> / <em>Reload</em>).</p>
|
|
|
|
<h3>Sorting</h3>
|
|
<p>Click a column header to sort by that column. Click again to
|
|
toggle direction. <kbd>Shift</kbd>-click another header to
|
|
add a secondary sort key.</p>
|
|
|
|
<h3>Filtering</h3>
|
|
<p>Type in the box under a column header to filter rows whose
|
|
value contains your text (case-insensitive). Same filter UI
|
|
for every column.</p>
|
|
|
|
<h3>Customizing the columns</h3>
|
|
<p>The default Master Deliverables List has columns for every
|
|
component of a tracking number
|
|
(<code>originator</code>, <code>phase</code>,
|
|
<code>project</code>, <code>area</code>,
|
|
<code>discipline</code>, <code>type</code>,
|
|
<code>sequence</code>, <code>suffix</code>) plus deliverable
|
|
metadata. To customize, drop your own
|
|
<code>table.yaml</code> (and matching
|
|
<code>form.yaml</code>) into this directory:</p>
|
|
<pre><code>archive/<party>/mdl/
|
|
table.yaml ← columns + sort/filter defaults
|
|
form.yaml ← per-row schema (JSON Schema)
|
|
<id>.yaml ... ← rows</code></pre>
|
|
<p>Operator-supplied files override the embedded defaults.
|
|
Hide a column by omitting it from <code>columns:</code>;
|
|
add a column by appending one (and adding the matching
|
|
property in <code>form.yaml</code>'s
|
|
<code>schema.properties</code>). The same pattern works
|
|
for any directory — <code><dir>/table.html</code>
|
|
is automatically a table whenever
|
|
<code><dir>/table.yaml</code> exists.</p>
|
|
|
|
<h3>Permissions</h3>
|
|
<p>Whether a row is editable depends on the cascading
|
|
<code>.zddc</code> permissions for the directory. Rows
|
|
in <code>Issued</code> or <code>Received</code> archives
|
|
are read-only by design (WORM).</p>
|
|
|
|
<h3>Header buttons</h3>
|
|
<dl>
|
|
<dt>◐ Theme</dt>
|
|
<dd>Cycle auto / light / dark.</dd>
|
|
<dt>? Help</dt>
|
|
<dd>This panel. Press <kbd>Esc</kbd> to close.</dd>
|
|
</dl>
|
|
</div>
|
|
</aside>
|
|
|
|
<!--
|
|
Server injects the table context here on render. Shape:
|
|
{
|
|
"title": "Optional page title override",
|
|
"description": "Optional description shown above the table",
|
|
"columns": [{field, title, width?, format?, filter?, sort?, enum?}],
|
|
"rows": [{url, data, editable}],
|
|
"defaults": {sort?: [{field, dir}], filter?: {field: value}}
|
|
}
|
|
-->
|
|
<script id="table-context" type="application/json">{}</script>
|
|
|
|
<!--
|
|
Form mode context — server injects this for /<dir>/form.html and
|
|
/<dir>/<id>.yaml.html. Empty in table-mode renders.
|
|
-->
|
|
<script id="form-context" type="application/json">{}</script>
|
|
|
|
<script>
|
|
{{JS_PLACEHOLDER}}
|
|
</script>
|
|
</body>
|
|
</html>
|