Skip to content

feat(ogar-render-askama): T4 — HtmlForm (Redmine _form.html.erb on ClassView) + cell dispatch factor#85

Merged
AdaWorldAPI merged 1 commit into
mainfrom
claude/ogar-render-askama-t4-html-form
Jun 20, 2026
Merged

feat(ogar-render-askama): T4 — HtmlForm (Redmine _form.html.erb on ClassView) + cell dispatch factor#85
AdaWorldAPI merged 1 commit into
mainfrom
claude/ogar-render-askama-t4-html-form

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

What

T4 (HtmlForm) per Northstar plan §3 — the create/edit-page sibling of T2 (list) and T3 (detail). Mirror of Redmine's _form.html.erb shape, generalised across every canonical concept via the codebook + ClassView substrate.

Same plumbing pattern as T2/T3 with a new dimension: InputKind parallels ColumnKind, but for form-input controls instead of display formatters.

New surface

API Purpose
ArtifactKind::HtmlForm New variant at slot 3 (append-only). ALL.len() 6 → 7.
form_view::InputKind 9-variant enum: Text, TextArea, Number, Range, Checkbox, Date, DateTime, Select, Hidden.
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.
InputData enum + SelectOptionOwned Owned data variant the caller supplies per field.
FormFieldSource / FormSource Public call-site shapes for one field and the whole form.
render_form(class_id, concept, &FormSource) -> Result<String> Substantive entry point.
HtmlFormEmitter Codebook-only proof-of-shape for for_kind.
empty_input_for(InputKind) Blank-form convenience.

Templates

  • 1 spine: dispatch/html_form.askama<form> + <fieldset> + per-field <div class="form-field-..."> wrappers (hidden inputs emitted bare).
  • 9 input sub-templates: dispatch/input/{text, textarea, number, range, checkbox, date, datetime, select, hidden}.askama — each escape = "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_cell and html_detail_view::render_cell_body now both call it.
  • T4 follows the same pattern with InputKind via inputs::render_input_body.

Tests (+13 for T4, 38/38 total)

  • form_view module (8): every default_input_kind_for arm + template_stem stability + html_input_type Some/None for TextArea/Select.
  • html_form_proof_of_shape_renders_inputs_for_class_attributes — codebook emit 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> shape.
    • Selected <option value="2" selected>In Progress</option>.
    • Rails idiom: checkbox carries the hidden 0 sibling so unchecked POSTs 0.
    • Hidden field has NO form-field-id wrapper (emitted bare).
    • Edit-form record_id hidden input.
    • CSRF token, legend, submit/cancel buttons all surface correctly.
  • html_form_escapes_data_derived_strings_xss_regression — same contract as T2/T3 spine fixes: label / hint / css_classes / action URL / legend / submit_label / placeholder / value / csrf_token all escape. action="/<bad-action>"action="/&lt;bad-action&gt;".
  • input_kind_resolver_is_wired_through_render_kit — pin the public re-export.
  • artifact_kind_all_const_enumerates_every_variantALL.len() 6 → 7.

Workspace check + workspace test green.

Status of the +5 kit after T4

PR Kind Flavour Status
T1 RustStruct codegen ✅ merged (#78)
T2 HtmlListView render ✅ merged (#83, then #84 fix)
T3 HtmlDetailView render ✅ merged (#84)
T4 HtmlForm render this PR
T5 SurrealqlTable codegen queued

4/5 real emitters after this merge. Remaining: T5 (DDL), plus the deprecated OpenapiSchema cleanup and the NodeGuidRoutingArm roadmap variant.

What this collapses

Redmine's _form.html.erb partial 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's type_name via default_input_kind_for.

…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="/&lt;bad-action&gt;".
- 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 }}">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@AdaWorldAPI AdaWorldAPI merged commit 1a6c5d1 into main Jun 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants