# ZDDC Tables

[← Back to ZDDC](../README.md)

Render a directory of YAML files as a sortable, filterable table — read-only, with click-row → edit-in-form integration. Backed by `zddc-server`'s form handler so the table view and the form editor are two sides of the same data.

**Anchor use case.** A Master Deliverables List (MDL) under `Archive/<Party>/MDL/`, where each `.yaml` file is one expected deliverable. Multiple parties keep their own MDLs side by side; the table aggregates within a single party's directory.

## How it works

- **Storage** is file-per-row YAML — one `*.yaml` file in a directory per table row. Concurrent edits don't collide on a shared blob, every row has independent git history, and per-row ACL inherits from the cascading `.zddc` chain.
- **Discovery** is `.zddc`-declarative. Drop a `tables:` entry in the directory's `.zddc` to register the table; no file-presence auto-mount, no phantom tables from rogue YAML drops.
- **Rendering** is server-side: `zddc-server` reads every `*.yaml` under the rows directory, normalizes them into a JSON list, and inlines the list into the page on render. The browser does sorting, filtering, and click-row navigation locally — no further server round-trips for those.
- **Editing** is delegated to the existing form tool. Each row's click target is the form's re-edit URL (`<dir>/<name>/<basename>.yaml.html`), which `zddc-server` already serves via the form handler. The table itself never writes.

## Setup (for an MDL at `Archive/Acme/MDL/`)

```
Archive/Acme/
├── .zddc                 # declares: tables: { MDL: ./MDL.table.yaml }
├── MDL.table.yaml        # column spec + rows path + row schema reference
├── MDL.form.yaml         # JSON Schema for one row (used by both the table and the form editor)
└── MDL/
    ├── D-001.yaml        # one row
    ├── D-002.yaml        # one row
    └── ...
```

Visit `Archive/Acme/MDL.table.html` and the table renders. Visit `Archive/Acme/MDL.form.html` to add a new row (the form handler creates a YAML in `MDL/`).

### `.zddc` declaration

```yaml
tables:
  MDL: ./MDL.table.yaml
```

The map key (`MDL`) becomes the URL stem and must match both the rows directory name and the form spec name. v1 enforces this with a load-time spec-validation error.

### Table spec (`MDL.table.yaml`)

```yaml
title: Master Deliverables List
description: Optional description shown above the table.
rowSchema: ./MDL.form.yaml         # path to the row's JSON Schema (form-spec format)
rows: ./MDL                        # directory of *.yaml row files (non-recursive in v1)
columns:
  - field: id                      # top-level key OR JSON Pointer (e.g. /nested/path)
    title: ID
    width: 7em
    sort: asc                      # default sort key (overridden by defaults.sort below)
  - field: title
    title: Deliverable
  - field: dueDate
    title: Due
    format: date                   # date | datetime | number | bool
  - field: status
    title: Status
    enum: [pending, submitted, accepted, rejected]   # constrains values + enables enum filter
defaults:
  sort:
    - { field: dueDate, dir: asc }
  filter:
    status: [pending, submitted]   # initial filter state; clear with the toolbar button
```

Columns are explicit — the renderer does not auto-derive from the row schema. Pick the subset you want to display.

## ACL behavior

- The page-level read check uses the cascade at the spec directory; a caller without `r` gets a 403.
- Per-row "edit" affordance is recomputed against the row's own parent dir. If the user has `w` there, the row is clickable; otherwise it's plain text. Hard enforcement remains on the form-handler side (the form's POST will refuse a write the cascade denies).
- `Issued`/`Received` archive folders are server-enforced WORM. The decider strips `w/d/a` from non-admin grants under those subtrees, so an MDL placed inside `Issued/` shows every row as read-only with no special-casing in the table tool.

## v1 limits

- Read-only grid; click-row opens the form editor. Inline cell editing is a v2 candidate (would PUT each edit through the new file API in `zddc/internal/handler/fileapi.go`).
- One directory of `*.yaml` per table; cross-directory aggregation (`Archive/*/MDL/*.yaml` as one combined view) is not yet supported.
- No virtualization — large tables (>1000 rows) will be slow.
- No multi-row bulk operations, no add-row UI inside the table (use the form editor at `<name>.form.html`).
- `.zddc tables:` declarations are direct-lookup only; no upward cascade. Each directory hosting a table needs its own declaration.

## Build & develop

```bash
sh tables/build.sh                       # build (writes tables/dist/tables.html)
sh tables/build.sh --release alpha       # cut alpha
sh ./build                               # full lockstep build (all tools + zddc-server)

(cd zddc && go test ./internal/handler/... ./internal/zddc/...)
npx playwright test --project=tables
```

Authoritative architecture and build docs are in [`../AGENTS.md`](../AGENTS.md) and [`../ARCHITECTURE.md`](../ARCHITECTURE.md).
