# `MishkaCmsCore.Runtime.PageBuilder.Section`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/page_builder/section.ex#L5)

What "the template" means for each of the four things the builder edits, and how to write one back.

Three of them — page, layout and component — keep their markup in a single `template` attribute
and need nothing special. **An error page does not.** It stores `error_pages`, a list of
`%{status, template, layout}` entries, so "open the error page in the builder" is really "open the
404 entry of this error page", and saving has to replace one element of that list rather than set
a field. Without this module the error-page route would appear to work and then write a `template`
attribute the resource does not have.

Keeping the difference here — rather than as branches inside the LiveView — is what lets the
editor, the canvas, the history and the save gate stay identical for all four sections.

# `allowed?`

```elixir
@spec allowed?(map() | nil, String.t() | atom()) :: boolean()
```

Whether `actor` may open the builder on `section` (`permissions/1`).

# `apply_fields`

```elixir
@spec apply_fields(map(), map() | nil) :: map()
```

Resolves what `save_fields/3` deferred against `record` — the row being written, which for a
create is `nil`.

Values that are not functions are passed through, so a caller with fields of its own can merge
them in before resolving.

# `atoms`

```elixir
@spec atoms() :: [atom()]
```

Every section as an atom — what a resource constraint or a struct's enum needs.

# `back_path`

```elixir
@spec back_path(String.t(), String.t() | nil) :: String.t()
```

Where the builder's back link goes: `return_to` when it names another builder, the section's list
otherwise.

THE ADDRESS IS SOMETHING A URL SUPPLIED, so it is matched on the builder's own prefix rather than
trusted. That prefix starts with a single `/`, which is what rules out `//elsewhere.example` and
`https://elsewhere.example` — a back link that could be pointed off-site is an open redirect with
the admin's session behind it.

# `builder_path`

```elixir
@spec builder_path(
  String.t() | atom(),
  String.t() | nil,
  MishkaCmsCore.Runtime.ErrorStatus.t() | nil
) ::
  String.t()
```

The builder's address for one document, or for making a new one when `id` is `nil`.

`status` names the error-page entry being edited. Without it an error page reopens on its 404
entry, which is not the one the author left.

THE ONE SPELLING. `back_path/2` honours an address by this prefix, so an address written out by
hand somewhere else is one the two could disagree about.

# `editing_status`

```elixir
@spec editing_status(String.t(), map() | nil, keyword()) ::
  MishkaCmsCore.Runtime.ErrorStatus.t() | nil
```

The status an editing SESSION is for, or `nil` for a section that has none.

`status/2` always answers a status, because an error page always opens on one. This is the other
question — "is a status part of what this document is?" — and for a page, a layout or a component
the answer is no. It is what keys an editing session, so a session opened on the 404 entry is not
handed back for the 500 one.

# `known?`

```elixir
@spec known?(String.t()) :: boolean()
```

Whether `section` is one the builder knows.

# `label`

```elixir
@spec label(String.t()) :: String.t()
```

A human name for the section, used in flash messages.

# `list_path`

```elixir
@spec list_path(String.t()) :: String.t()
```

Where the section's index page lives — the builder's back link.

# `names`

```elixir
@spec names() :: [String.t()]
```

The section names the builder can open.

# `normalize`

```elixir
@spec normalize(String.t() | nil | atom()) :: String.t()
```

Normalizes `page`, `error_page` and `error-page` to the canonical hyphenated form.

# `offered_statuses`

```elixir
@spec offered_statuses() :: [MishkaCmsCore.Runtime.ErrorStatus.t()]
```

The statuses an error page can hold an entry for: the offline page, then the codes the Error Pages
offer (`MishkaCmsCore.Runtime.ErrorStatus.choices/0`). One the record lacks is added by opening it
in the builder.

# `permissions`

```elixir
@spec permissions(String.t() | atom()) :: [String.t()]
```

The names that open the builder on `section`, holding any one being enough: its own runtime area
or `admin:dashboard:code`.

# `read_opts`

```elixir
@spec read_opts(module(), map() | nil) :: {:ok, keyword()} | :error
```

How `actor` reads one document of `resource`, with its site loaded: `{:ok, opts}` for `Ash.get/3`,
`:error` for a viewer who is not a user.

A master reads across sites with `:master_get`, multitenancy bypassed. A site admin reads with
their site as the tenant — through `:get_for_tenant_with_globals` where the resource declares it,
because a layout or a component may belong to no site at all, and `global? true` only means the
resource may be read WITHOUT a tenant: a tenant-scoped read still filters a site-less row out. A
resource with no shared rows — a page, an error page — keeps the plain tenant read.

# `resource`

```elixir
@spec resource(String.t()) :: module()
```

The Ash resource behind a section.

# `save_fields`

```elixir
@spec save_fields(String.t(), String.t(), keyword()) :: %{
  required(String.t()) =&gt; term()
}
```

The attribute map to hand `MishkaCmsCore.Runtime.PageBuilder.Helpers.save_section/4`, with the
values that depend on the record left as functions of it.

For an error page the whole `error_pages` list IS the attribute, so the write is every entry with
one of them replaced — and an entry this session is not editing is only carried across correctly
if the list it is copied from is the STORED one. A record read earlier is not: the builder holds
`@record`, the snapshot its LiveView mounted on, and a tab sitting on the 404 since before the 500
was published wrote the old 500 back over the new one, reverting a publish with nothing said. So
the value is deferred rather than computed, and `apply_fields/2` resolves it against the row the
write has just read and is about to update.

Every other section sets `template` outright and never reads the record, so its value is the
markup and nothing about it can go stale.

# `sibling_templates`

```elixir
@spec sibling_templates(String.t(), map() | nil, keyword()) :: [String.t()]
```

The templates this record owns OTHER than the one being edited.

Empty for every section but an error page, which keeps one template per status in a single record.
That matters wherever something is keyed to the RECORD rather than to the status — the element-CSS
row is, and pruning it against only the open status deleted the rules belonging to the other four.

# `status`

```elixir
@spec status(map() | nil, keyword()) :: MishkaCmsCore.Runtime.ErrorStatus.t()
```

The status the editor should open on: the requested one if the record has it or an error page can
hold it (`offered_statuses/0`, which is how a status is added), else the first.

# `statuses`

```elixir
@spec statuses(String.t(), map() | nil) :: [MishkaCmsCore.Runtime.ErrorStatus.t()]
```

The statuses an error-page record defines, codes in ascending order and then `:offline`. `[]` for
every other section — the status tab strip only renders when there is something to switch between.

# `statuses`

```elixir
@spec statuses(String.t(), map() | nil, MishkaCmsCore.Runtime.ErrorStatus.t() | nil) ::
  [
    MishkaCmsCore.Runtime.ErrorStatus.t()
  ]
```

The statuses the builder's strip shows for a record: those it defines and the one open, which may be
one being added. `[]` for every other section and for a document not yet written.

# `template`

```elixir
@spec template(String.t(), map() | nil, keyword()) :: String.t()
```

The markup the editor should open for this record.

For an error page, `opts[:status]` selects which status entry — defaulting to 404, or the first
entry the record actually has.

# `to_atom`

```elixir
@spec to_atom(String.t() | nil | atom()) :: {:ok, atom()} | :error
```

The section as the atom the log and the resources use, or `:error` for anything else.

Total on purpose: the argument is often something a URL supplied.

# `to_atom!`

```elixir
@spec to_atom!(String.t() | nil | atom()) :: atom()
```

The same, for a section already known to be one of ours. Raises rather than inventing one.

# `to_underscored`

```elixir
@spec to_underscored(String.t() | nil | atom()) :: String.t()
```

The underscored form — what an editing session stores and what the resources spell their sections
with.

ONE SECTION IS SPELLED TWO WAYS. The address says `error-page` and everything durable says
`error_page`, and that difference was converted by hand in nine places across five modules. Three
of them were bare `String.to_existing_atom/1` calls, which is not a conversion but a crash waiting
for the one section whose two spellings differ — it took the builder's mount down for error pages.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
