MishkaCmsCore.Runtime.PageSamples (Mishka CMS Core v0.0.1)

Copy Markdown View Source

What a site's sample pages are against the ones this checkout ships, and the one write that offers the newer ones without taking an admin's page away from them.

The gap this closes

An extension's "Set up …" button writes only the pages a site has no row for, deliberately — see MishkaCmsCore.Runtime.PageProvision — so a site provisioned once keeps the pages it was provisioned with for ever. MishkaCmsCore.Runtime.Handlers.UiKitUpgrade closed the same gap for COMPONENTS, and closing it left the other half wide open: the blog's featured strip became a component of its own, the index page that draws it was rewritten to place it, and no site already serving that page ever saw the new one.

Why a draft, and not an update

A live page is the admin's. They may have rewritten it, hung their own blocks on it, or simply be serving it to readers right now — and nothing here can tell a page that is BEHIND from one somebody EDITED, because a page carries no fingerprint of what it was created from. An update that guessed would publish this module's markup over somebody's work, live, with no step in between.

So the offer is a MishkaCmsCore.Runtime.PageDraft of the page itself: it appears in the builder's draft picker next to the admin's own drafts, they open it, look at it, and publish it if they want it. Nothing live changes until they do, and publishing keeps a version of what was there through the page's own paper trail.

The answers

plan/2 sorts every page an extension ships into:

  • :current — the row holds exactly the template this checkout ships;
  • :outdated — it does not, and no draft of this version's page is waiting on it. This is what the button writes;
  • :offered — a draft carrying this version's template is already pending on that page, so there is nothing to write. This is what makes a second press do nothing;
  • :blocked — it is behind, and the page this version ships draws a component this site cannot draw. See below;
  • :taken — a page that is not ours answers at that address;
  • :archived — a page of ours is at that address and the admin archived it;
  • :absent — nothing is at that address at all, which is the "Set up …" button's job and never this one's.

The last three are one answer in the site's eyes and three different dead ends in the admin's: the "Set up …" button writes the absent one and skips the other two, so a card that called all three absent would send an admin to a button that does nothing for two of them.

A row is ours when its ADDRESS and its SECTION are both the definition's. The address alone would claim a page an admin wrote at the same path as their own work and offer to replace it; the section is what an extension stamps every page it writes with, and a page whose section an admin has changed is one this cannot recognise — so it is left alone, which is the safe way round.

Why a page can be held back

A page is markup that names components by name, and the name resolves against the site's own rows at render time: MishkaCmsCore.Runtime.LiveViewHelpers draws a red "Component failed to render!" line in place of one it cannot build. This version's blog index draws blog-featured-posts, which is newer than the sites the offer exists for, and no update installs a component a site does not have — the "Set up …" button does. So offering that page to a site without it would be offering an admin a draft whose whole purpose is to put an error message in front of their readers.

:blocked is the answer instead, naming the components that are not there. Nothing is written for such a page at any setting.

Pressing it twice

offer!/3 reads the plan again inside the call rather than taking it from the caller, and writes one draft per :outdated page. On the second press those pages are :offered and nothing is written. An admin who then EDITS the draft moves it off this version's template, so the offer is made again — that is the honest answer to "is a draft of the shipped page waiting here", and the second draft costs them nothing but a row in the picker.

Summary

Types

One shipped page and its standing. name is the page's ADDRESS: that is what an admin recognises a sample page by, and what every report of one is written from. needs names the components this version's page draws that the site cannot draw, and is empty wherever there are none.

Where one shipped page stands on this site. :offered is not a kind of :outdated — it is the work already done, and a button that offered it again would leave the picker full of copies.

Functions

What one offer did, worded from what was actually written.

The line a dashboard shows beside one page, saying what its standing is.

Offers every outdated page as a draft of itself and says what it did.

Where every page in definitions stands on site_id. Reads, and writes nothing at all: this is what a dashboard shows an admin BEFORE anything happens.

Types

entry()

@type entry() :: %{
  name: String.t(),
  standing: standing(),
  page_id: String.t() | nil,
  needs: [String.t()]
}

One shipped page and its standing. name is the page's ADDRESS: that is what an admin recognises a sample page by, and what every report of one is written from. needs names the components this version's page draws that the site cannot draw, and is empty wherever there are none.

plan()

@type plan() :: %{
  site_id: String.t(),
  serves_pages?: boolean(),
  current: [entry()],
  outdated: [entry()],
  offered: [entry()],
  blocked: [entry()],
  taken: [entry()],
  archived: [entry()],
  absent: [entry()]
}

report()

@type report() :: %{
  drafted: [String.t()],
  waiting: [String.t()],
  current: [String.t()],
  blocked: [String.t()],
  taken: [String.t()],
  archived: [String.t()],
  absent: [String.t()]
}

standing()

@type standing() ::
  :current | :outdated | :offered | :blocked | :taken | :archived | :absent

Where one shipped page stands on this site. :offered is not a kind of :outdated — it is the work already done, and a button that offered it again would leave the picker full of copies.

Functions

message(report)

@spec message(report()) :: String.t()

What one offer did, worded from what was actually written.

The sentence about nothing living having changed is not decoration: an admin who pressed a button on a dashboard and was told "3 pages" has every reason to think their site just changed, and the whole point of this path is that it did not.

note(map)

@spec note(entry()) :: String.t()

The line a dashboard shows beside one page, saying what its standing is.

An :outdated page is not called old. Nothing here can tell a row that is behind from one an admin rewrote, and a card that called somebody's own work outdated would be lying to them in the one sentence that has to be trusted.

offer!(site_id, definitions, opts \\ [])

@spec offer!(String.t(), [map()], keyword()) :: report()

Offers every outdated page as a draft of itself and says what it did.

:label is what the drafts are called in the builder's picker; it defaults to "Sample page".

Writes nothing but MishkaCmsCore.Runtime.PageDraft rows. No live page is touched here at any setting — publishing one is the admin's own gesture, in the builder, one page at a time.

Pass actor: user and every draft is written as them, so its history says who forked the page.

plan(site_id, definitions, actor \\ nil)

@spec plan(String.t(), [map()], map() | nil) :: plan()

Where every page in definitions stands on site_id. Reads, and writes nothing at all: this is what a dashboard shows an admin BEFORE anything happens.

definitions are the attribute maps a page create takes — the same ones the extension hands MishkaCmsCore.Runtime.PageProvision — so the two can never be describing different pages.