feat(ogar-render-askama): T4 — HtmlForm (Redmine _form.html.erb on ClassView) + cell dispatch factor#85
Conversation
…assView) Fourth real emitter per Northstar plan §3. Mirror of Redmine's _form.html.erb shape: one shared form template per resource, here substrate-agnostic via the codebook + ClassView. Same plumbing as T2/T3 with a new dimension (InputKind for form-input controls, parallel to ColumnKind for cell formatters). Surface (additive): - ArtifactKind::HtmlForm variant (slot 3, append-only). ALL.len() now 7. - form_view::InputKind — 9-variant enum: Text, TextArea, Number, Range, Checkbox, Date, DateTime, Select, Hidden. Append-only; template stems stable. - form_view::default_input_kind_for(name, type_name) — resolver mirroring list_view::default_kind_for. id/*_id → Hidden, *_ratio → Range, text+prose → TextArea, boolean → Checkbox, date/datetime → date inputs, numeric Rails types → Number, else → Text. - artifact_kinds::inputs — one binding-struct + askama template per InputKind (9 total). Each escape="html" so per-input variables are HTML-escaped; spine concats with |safe. - artifact_kinds::inputs::InputData — owned data variant the caller supplies, keyed by InputKind. Carries value, required, options, etc. - artifact_kinds::html_form::FormFieldSource + FormSource — the public call-site shapes for one field and the whole form. - artifact_kinds::html_form::render_form(class_id, concept, &FormSource) — the substantive entry point. - HtmlFormEmitter — codebook-only proof-of-shape for the for_kind dispatch path; synthesises a new-record form from class attributes. - empty_input_for(InputKind) — convenience for callers that want a blank form. - templates/dispatch/html_form.askama — spine; <form> + <fieldset> + per-field <div class="form-field-…"> wrappers (hidden fields emitted bare without wrapper). - 9 templates/dispatch/input/*.askama: text, textarea, number, range, checkbox, date, datetime, select, hidden. Refactor — cell-dispatch factored: - New artifact_kinds::cells::render_cell_body(&CellData) → String — shared per-kind dispatch. T2's render_cell and T3's render_cell_body now call it. T4 follows the same pattern with InputKind via inputs::render_input_body. "Three points form a line" (Northstar §1.6 notice from #84). Tests (+13, 38/38 total): - form_view::tests (8): template stems stable, html_input_type None-vs-Some, every default_input_kind_for arm (Hidden / Range / TextArea / Number / Checkbox / Date / DateTime / Text fallback). - html_form_proof_of_shape_renders_inputs_for_class_attributes — proof of shape on project_role; data-class-id="0x0117" + per-attribute form-field-<name> wrappers + POST/Create defaults. - html_form_dispatches_input_kinds_to_their_sub_templates — explicit field per kind; pins every <input type=…>, textarea, select option selected="…", checkbox hidden-zero idiom, date/datetime types, hidden-without-wrapper, edit-form record_id, CSRF token, legend, submit/cancel buttons. - html_form_escapes_data_derived_strings_xss_regression — same XSS contract as T2/T3 spines: label / hint / css_classes / action / legend / submit_label / values / placeholder / csrf_token all escape; action="/<bad-action>" → action="/<bad-action>". - input_kind_resolver_is_wired_through_render_kit — pin the public re-export. - artifact_kind_all_const_enumerates_every_variant — ALL.len() 6 → 7. Workspace check + workspace test green. 4/7 real emitters now (T1 RustStruct, T2 HtmlListView, T3 HtmlDetailView, T4 HtmlForm); 3 stubbed for T5 (SurrealqlTable) and the two roadmap-only / deprecated variants.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d967c3e7a5
ℹ️ 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".
| {# driven by `fields` (per-attribute pre-rendered <input>s). #} | ||
| {# Pre-rendered field bodies are |safe (each sub-template escapes its #} | ||
| {# own variables); everything else is HTML-escaped by the spine. #} | ||
| <form class="ogar-form" data-class-id="{{ class_id_hex }}" data-concept="{{ canonical_concept }}" method="{{ method }}" action="{{ action }}"> |
There was a problem hiding this comment.
Use POST method overrides for PATCH edits
When callers follow the new FormSource contract for edit forms and pass method: "patch", this template emits that value directly on the <form>. Native HTML form submission only supports GET/POST (ignoring framework-specific JS), so an edit form rendered this way will not submit as a PATCH to /issues/42; Rails-style/Redmine-style forms need method="post" plus a hidden method override such as _method=patch for updates.
Useful? React with 👍 / 👎.
What
T4 (HtmlForm) per Northstar plan §3 — the create/edit-page sibling of T2 (list) and T3 (detail). Mirror of Redmine's
_form.html.erbshape, generalised across every canonical concept via the codebook + ClassView substrate.Same plumbing pattern as T2/T3 with a new dimension:
InputKindparallelsColumnKind, but for form-input controls instead of display formatters.New surface
ArtifactKind::HtmlFormALL.len()6 → 7.form_view::InputKindText,TextArea,Number,Range,Checkbox,Date,DateTime,Select,Hidden.form_view::default_input_kind_for(name, type_name)list_view::default_kind_for.id/*_id→ Hidden,*_ratio→ Range,text+prose→ TextArea,boolean→ Checkbox,date/datetime→ date inputs, numeric Rails types → Number.InputDataenum +SelectOptionOwnedFormFieldSource/FormSourcerender_form(class_id, concept, &FormSource) -> Result<String>HtmlFormEmitterfor_kind.empty_input_for(InputKind)Templates
dispatch/html_form.askama—<form>+<fieldset>+ per-field<div class="form-field-...">wrappers (hidden inputs emitted bare).dispatch/input/{text, textarea, number, range, checkbox, date, datetime, select, hidden}.askama— eachescape = "html", called once-per-field at build time, output threaded into the spine with|safe.Refactor (the "three points form a line" piece)
T4 became the third caller of the per-kind cell dispatch (after T2 and T3 duplicated it). Factored into one helper:
artifact_kinds::cells::render_cell_body(&CellData) -> Result<String>— shared dispatch.html_list_view::render_cellandhtml_detail_view::render_cell_bodynow both call it.InputKindviainputs::render_input_body.Tests (+13 for T4, 38/38 total)
default_input_kind_forarm +template_stemstability +html_input_typeSome/None for TextArea/Select.html_form_proof_of_shape_renders_inputs_for_class_attributes— codebook emit onproject_role(data-class-id="0x0117"); per-attributeform-field-<name>wrappers; POST + "Create" defaults.html_form_dispatches_input_kinds_to_their_sub_templates— explicit field per kind; pins:<input type=…>/<textarea>/<select>shape.<option value="2" selected>In Progress</option>.0sibling so unchecked POSTs0.form-field-idwrapper (emitted bare).record_idhidden input.html_form_escapes_data_derived_strings_xss_regression— same contract as T2/T3 spine fixes: label / hint / css_classes /actionURL / legend / submit_label / placeholder / value / csrf_token all escape.action="/<bad-action>"→action="/<bad-action>".input_kind_resolver_is_wired_through_render_kit— pin the public re-export.artifact_kind_all_const_enumerates_every_variant—ALL.len()6 → 7.Workspace check + workspace test green.
Status of the +5 kit after T4
4/5 real emitters after this merge. Remaining: T5 (DDL), plus the deprecated
OpenapiSchemacleanup and theNodeGuidRoutingArmroadmap variant.What this collapses
Redmine's
_form.html.erbpartial set: 26 controllers × 1 form partial ≈ 26 form files, each ~80–200 LOC with hand-laid<input>declarations +<%= form.text_field :foo %>calls. T4 collapses that to 1 spine template + 9 input sub-templates (~70 LOC of templates), with the per-input control resolved at runtime from the canonical attribute'stype_nameviadefault_input_kind_for.