docs(ogar): Redmine Query/QueryColumn harvest — concrete T2 spec#82
Conversation
"Apple meets Apple." Redmine spent 17 years iterating to a clean dynamic-column render pattern (Query + QueryColumn + helpers); their _list.html.erb is substrate-agnostic and dispatches every list page in the app through one body. Same insight WoA-rs codified and the Northstar plan calcifies. We harvest, don't reinvent. Adds docs/integration/REDMINE-QUERY-HARVEST.md (sibling to the Northstar plan). Captures: §1 The Redmine pattern they evolved: - QueryColumn with 8 render-meta properties (name, caption, sortable, groupable, totalable, default_order, inline, frozen). - Specialised subclasses: TimestampQueryColumn, WatcherQueryColumn, QueryAssociationColumn, QueryCustomFieldColumn (+ chained variant). - Per-resource Query subclass declares ~25-column `available_columns` list. 6 subclasses today (IssueQuery / ProjectQuery / …). - Three render helpers: column_header / column_content / column_value. column_value is a 12-arm case statement — the formatter library. - One spine partial (_list.html.erb, 61 LOC) renders every list page. §2 Map onto ClassView (point-by-point): - query.inline_columns / block_columns → FieldMask & inline_set / block_set. - QueryColumn.name → FieldRef.predicate_iri. - QueryColumn.caption → FieldRef.label. - Render-meta (sortable / groupable / …) → RenderColumn sidecar in the binding-struct, NOT extending FieldRef (per Northstar anti-pattern #2). - Cell formatters → per-kind askama sub-templates dispatched via ColumnKind enum (append-only). - 6 Query subclasses → 0 (ClassView is the spine). §3 Concrete T2 spec: - HtmlListViewCtx binding-struct shape + RenderColumn + ColumnKind + … - Template skeleton (mirror of _list.html.erb body, FieldMask-driven). - ~10 cell sub-templates one-per-ColumnKind (id_link, primary_link, record_ref, rich_text, progress_bar, relation_list, hours, attachment_list, user_list, plain). Append-only. - Pre-render cells in Rust at row-build time; spine template emits `{{ cell.body_html|safe }}` — no runtime polymorphism in askama. §4 TODO surface for T2's PR. §5 Where each piece lives across crates. §6 The Apple-meets-Apple summary — 26 partials × 6 Query subclasses → 1 spine template × 0 subclasses, with the typed canonical layer replacing untyped jsonb + Ruby helpers + per-controller views. doc-only; no production code touched.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 52ad220adf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| > column-dispatch render pattern (`Query` + `QueryColumn` + helper trio). | ||
| > This doc harvests it as a *spec*, maps each piece onto our | ||
| > `ClassView` / `ogar-vocab::Class` / `ogar-render-askama` substrate, and | ||
| > derives the concrete shape of T2 (`HtmlListView`) so the askama template |
There was a problem hiding this comment.
Keep T2 aligned with the Northstar plan
This document repeatedly defines T2 as HtmlListView, but the plan it tells readers to use as the source of truth lists T2 as TsInterface in docs/integration/CLASSVIEW-MATERIALIZATION-PLAN.md §3, with T3–T5 already assigned to the remaining artifact emitters. In the next implementation cycle, following this new TODO would cause the implementer to build the wrong PR and skip the planned TypeScript emitter, so either rename this work item or update the Northstar plan in the same change.
Useful? React with 👍 / 👎.
| /// The pre-formatted body of the cell. For `Plain` kind this is the | ||
| /// late-resolved string from RenderRow; for richer kinds, the sub- | ||
| /// template's already-rendered output. | ||
| body_html: askama::filters::SafeString, |
There was a problem hiding this comment.
Do not mark plain cell strings as safe HTML
When a generic list renders Plain cells containing user-controlled string/text values, this spec stores the late-resolved string in SafeString and the template later emits it with |safe, which bypasses Askama's HTML escaping for those cells. The rich sub-template outputs can be marked safe after they escape their own inputs, but plain values should stay as escaped text or carry an explicit escaped-vs-HTML distinction before the implementation copies this binding shape.
Useful? React with 👍 / 👎.
What
Harvests Redmine's
Query+QueryColumn+ helpers pattern as the concrete spec for T2 (HtmlListView). Sibling to the Northstar plan (CLASSVIEW-MATERIALIZATION-PLAN.md); doc-only, 481 LOC.The headline finding
Redmine's
app/views/issues/_list.html.erb(61 LOC) is substrate-agnostic. It iteratesquery.inline_columns, callscolumn_header(query, col)+column_content(col, item), and renders every list page in the app. They didn't write it per controller — they wrote it once.That's the same insight WoA-rs codified with
RouteSpec/HandlerKindand our Northstar plan codifies withClassView/ArtifactKind. The design work for the HTML render side is done; we map their shape onto our substrate.What the harvest captures
§1 — What Redmine evolved
QueryColumnwith 8 render-meta properties (sortable, groupable, totalable, inline, frozen, default_order, caption, name) — the exact per-column metadata a generic list view needs.TimestampQueryColumn,WatcherQueryColumn,QueryAssociationColumn,QueryCustomFieldColumn) covering the irregular shapes.Querysubclasses each declaring ~25 columns = ~150 column declarations.column_header/column_content/column_value(the last is a 12-armcase— the cell formatter library, 17 years' worth).§2 — Map onto ClassView (point-by-point)
query.inline_columns/block_columnsFieldMask & inline_set/block_setQueryColumn.name/.captionFieldRef.predicate_iri/.labelRenderColumnsidecar in binding-struct (not on contract — anti-pattern #2)column_value12-armcaseColumnKindenum + per-kind askama sub-templates§3 — Concrete T2 spec
HtmlListViewCtxbinding-struct +RenderColumn+ColumnKindenum + askama template skeleton + ~10 cell sub-templates (one per kind:IdLink,PrimaryLink,RecordRef,RichText,ProgressBar,RelationList,Hours,AttachmentList,UserList,Plain).Cells are pre-rendered in Rust at row-build time — the spine template emits
{{ cell.body_html|safe }}, no runtime polymorphism in askama.§4 — TODO surface for T2's PR
Five concrete checkboxes for the next implementation PR.
§6 — The Apple-meets-Apple summary
caseformatterWhy land this as a doc PR first
T2's PR will be substantial (binding-struct + ~10 templates + emitter + tests). Landing the spec first lets the implementation hew to a written contract, and gives codex / future sessions the harvested rationale instead of having to re-derive from
app/views/issues/_list.html.erb.Scope
Doc-only. No production code, no test changes.
docs/integration/REDMINE-QUERY-HARVEST.mdis additive.