MishkaCmsCore.Runtime.PageBuilder.Section (Mishka CMS Core v0.0.1)

Copy Markdown View Source

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.

Summary

Functions

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

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

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

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

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

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

Whether section is one the builder knows.

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

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

The section names the builder can open.

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

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.

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

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.

The Ash resource behind a section.

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.

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

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.

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.

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.

The markup the editor should open for this record.

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

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

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

Functions

allowed?(actor, section)

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

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

apply_fields(fields, record)

@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()

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

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

back_path(section, return_to)

@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(section, id \\ nil, status \\ nil)

@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(arg1, record, opts)

@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?(section)

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

Whether section is one the builder knows.

label(section)

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

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

list_path(section)

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

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

names()

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

The section names the builder can open.

normalize(section)

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

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

offered_statuses()

@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(section)

@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(resource, actor)

@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(section)

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

The Ash resource behind a section.

save_fields(arg1, heex, opts)

@spec save_fields(String.t(), String.t(), keyword()) :: %{
  required(String.t()) => 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(arg1, record, opts)

@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(record, opts)

@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(arg1, record)

@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(section, record, open)

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(arg1, record, opts)

@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(section)

@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!(section)

@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(section)

@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.