MishkaCmsCore.Runtime.PageBuilder.PageForms.FormChecks (Mishka CMS Core v0.0.1)

Copy Markdown View Source

The Forms group of the audit rail: what is wrong with the page's forms, and the tree edit that puts each one right.

Every finding carries the shape the rail already renders — node_id, group, severity, rule, message, detail — so a form check reads beside an accessibility one and its Fix button is the same button. What is different is that most of these have a mechanical answer: repair/3 hands back the ops that fix the document, and the few with no edit behind them say why rather than appearing to work.

What is asked of each node

Not asked here at all: MishkaCmsCore.Runtime.PageBuilder.Fields.form_node?/2 and MishkaCmsCore.Runtime.PageBuilder.Fields.field_node?/2 are the editor's one answer to both, so what the insert funnel treats as a form, what MishkaCmsCore.Runtime.PageBuilder.Containment refuses to nest, and what the audit reports on cannot disagree. This module used to carry its own copy of each, and so did the other two.

Two questions about a submit button, two answers

form-no-submit asks, per node and at any depth inside the form, whether anything sends it (Fields.submit_node?/3): a bare <button>, a kit button left to its default, the CMS's own <.button>. A button whose type is computed (Fields.type_unread?/3) may send it, so the rule stays quiet rather than offer a second Send button. submit-no-form asks the reverse about a button outside every form, and there only a written type="submit" or "image" counts (Fields.explicit_submit?/3), so a hero or dialog button stays quiet.

Why a wrap is an insert and a delete

MishkaCmsCore.Runtime.PageBuilder.Op's :wrap builds a bare tag with no attributes, and a form is nothing without its five. There is also no id to write them onto: the wrapper is minted inside the combinator. So the field's own source is re-parsed, wrapped and bound in one go by FormPlacement.wrap/3, inserted where the field stood, and the field deleted — two ops in one MishkaCmsCore.Runtime.PageBuilder.EditorSession.apply_ops/4 run, which is one undo step.

A Fix that cannot act says so

Two of these have no edit behind them, and each says why in a sentence the author reads instead of appearing to work: there is no form on the page to move a button into (submit-no-form); unwrapping this form would strand the fields inside it (form-in-form). repair/3 answers {:say, sentence} for those, which is not the same as :none — :none is a stale click, and the assistant is the right place for a document that has moved on. Everything else here has exactly one right answer and takes it.

A note for developers

form-no-destination is a :note, not counted against the page: a form nobody wrote code for still checks its fields and thanks the visitor. A form that names no submit event and posts to its own action has somewhere to go, so it raises nothing (receiver/2).

Its Fix finishes the form first, one edit per click: its name (as), then its source (for), then its submit event. A form with all three answers {:code, event, sentence}: the form's submit event, for the editor to open in the Code window, and the sentence to show someone who cannot write code.

See MishkaCmsCore.Runtime.PageBuilder.PageForms for what declares a form in the first place, and MishkaCmsCore.Runtime.PageBuilder.PageForms.Naming for the five names a seed derives.

Summary

Functions

Every Forms finding this document raises, in document order, with the duplicate-name blockers last.

Whether these nodes hold a form at all.

What receives this form's submissions, the check form-no-destination makes

The ops that fix rule on node_id, the sentence to show when there is nothing this can edit, or :none.

The ops one of a finding's fixes makes, when it offers more than one: "typing" on form-no-submit makes the form work as they type (FormPlacement.how_ops/3). Any other fix is repair/3's.

Whether rule names one of this module's findings.

Functions

findings(tree, record, session)

@spec findings(MishkaCmsCore.Runtime.PageBuilder.Tree.t(), map() | nil, map()) :: [
  map()
]

Every Forms finding this document raises, in document order, with the duplicate-name blockers last.

record is the page being edited, read for the event handlers it declares; session is the editing session, read for the palette and for the section — a section whose compiler emits no form keeps only the rules HTML itself is wrong about.

forms?(nodes, session)

Whether these nodes hold a form at all.

The rail's passing rows are conditional on it: "every form reaches a handler" is not a tick a page with no form has earned, it is a check that never ran — and on a section that compiles no form it is not a check at all.

receiver(node, record)

@spec receiver(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), map() | nil) ::
  :code | :plugin | :action | :none

What receives this form's submissions, the check form-no-destination makes:

  • :code when a handler record stores answers the form's submit event (PageForms.answered?/2) with code that is not the untouched default (LiveViewCompiler.untouched_handler?/3);
  • :plugin when no such handler does and a plugin listens on page:form:submitted;
  • :action when the form posts to its own action: with no submit event, or once its answers are checked (PageForms.sends_elsewhere?/1);
  • :none otherwise.

repair(session, rule, node_id)

@spec repair(map(), String.t(), String.t() | nil) ::
  {:op, map()}
  | {:ops, [map()]}
  | {:say, String.t()}
  | {:code, String.t(), String.t()}
  | :none

The ops that fix rule on node_id, the sentence to show when there is nothing this can edit, or :none.

{:ops, ops} is a run applied as one history entry; {:op, op} is a single edit; {:say, sentence} is a refusal the author reads, for the fixes whose answer is not in this document at all. A rule this module does not answer for, and a node that is no longer in the tree, both give :none — the caller falls back to the assistant, which is the right answer for a stale click.

repair(session, rule, node_id, arg4)

@spec repair(map(), String.t(), String.t() | nil, String.t() | nil) ::
  {:op, map()}
  | {:ops, [map()]}
  | {:say, String.t()}
  | {:code, String.t(), String.t()}
  | :none

The ops one of a finding's fixes makes, when it offers more than one: "typing" on form-no-submit makes the form work as they type (FormPlacement.how_ops/3). Any other fix is repair/3's.

rule?(arg1)

@spec rule?(term()) :: boolean()

Whether rule names one of this module's findings.

Fix on such a finding is answered by repair/3; Fix on any other is handed to the assistant.