Plugin events

v0.0.1

Mishka Document talks to plugins through mishka_installer events. A plugin adds facts and tabs to a page, changes a page's head, adds buttons to the sections screen, or reacts when a record is saved or a version is copied. The extension also answers events of the CMS, to read a page's rows, fill its languages and its bar, and write its head. This reference lists each event, the data it carries and the shape of an answer. With no plugin, or an event that cannot run, every screen is drawn as it was.

Events at a glance

EventFiredA plugin can
document:page:extrasEach time doc-section reads a page or a document's landingAdd facts under the title and tabs after the text
document:page:seoWhen a document's or a page's head is builtChange the title, description, tags, structured data or share card
document:workspace:admin_actionsWhen the sections screen loads a documentAdd buttons that open a panel of its own
document:version:clonedAfter a version is copiedReact. The answer is not used.
document:version:detachedAfter a copy is made independentReact. The answer is not used.
document:<resource>:after_<action>After any create, update or destroy action of a document resourceReact to the saved record
document:<resource>:after_saveAfter any create or update, a master's twin includedReact once, however the record was written

document:page:extras

The data is a map. A plugin appends to header and tabs and replies with the map.

KeyHolds
site_idThe site's id.
document%{slug, path, language}
versionThe version number being read.
page%{slug, title, path, parents}, where parents lists %{slug, title, group} nearest last. It is nil on a document's landing.
headerThe facts, in the order they are drawn under the title.
tabsThe tabs drawn after the page's text.
elixir
use MishkaInstaller.Event.Hook, event: "document:page:extras"

def call(%{document: %{slug: "handbook"}, header: header} = state),
  do: {:reply, %{state | header: header ++ [%{kind: :text, label: "Edition", value: "2"}]}}

def call(state), do: {:reply, state}

The event is also asked by doc-section-cards, once for each card, with card: true. A plugin that sees it answers from what it already holds and only the badges of the header are drawn.

Header facts

KindShapeNotes
:badges%{kind: :badges, label: nil, items: [%{text: "v1.0", tone: :primary}]}A tone is neutral (the default), primary, secondary, success, info, warning, danger, or any other word a stylesheet keys on.
:command%{kind: :command, label: "Install", value: "mix ..."}Drawn left to right, with a copy button.
:link%{kind: :link, label: "Source", href: "https://...", text: "View source", rel: "nofollow"}href is http, https or a path on the site. An http or https link opens in a new tab. rel is optional.
:text%{kind: :text, label: "Package", value: "..."}
:pending%{kind: :pending, text: "..."}A skeleton for facts still being fetched. text is for a screen reader.

Tabs

A tab is %{id: "props", label: "Props", blocks: [block]}, or the one block it is. Its id is lower-case letters, digits, - and _, and never documentation. The first tab with an id is kept. Each tab links to the page with ?tab=<id>, and the page's own text shows when the URL names no tab a plugin added.

BlockShape
Table%{kind: :table, title: nil, description: nil, columns: [%{key: :name, label: "Name", code: true}], rows: [%{name: "color"}], empty_text: "..."}. A cell is a word, a list of words, or a badge such as %{text: "Yes", tone: :success}.
List%{kind: :list, title: nil, description: nil, items: [fact]}

A tab whose data is still being fetched is %{id: "props", label: "Props", state: :not_ready, text: "...", failed_text: "...", pending: %{topic: "..."}}. A tab whose data could not be had is %{id: "props", label: "Props", state: :failed, text: "..."}.

A pending topic is a Phoenix.PubSub topic on MishkaCmsCore.PubSub. The plugin announces the end of its fetch on it. Once the page is connected, doc-section starts one task that asks the event again when the topic speaks, and waits up to pending_wait milliseconds, 120000 by default. A wait that runs out turns each tab still being fetched into its failed_text. Nothing polls.

document:page:seo

The data is the head's map. Before the event, the extension fills its title, description, type, language, tags, schema, permissions, url, vars, var_docs and card, and a plugin's reply is the answer. A plugin that registers call/2 is also handed this meta:

KeyHolds
site_idThe site's id.
document%{name, slug, path, language, description, url}
versionThe version number, or nil when there is no published version.
page%{slug, title, description, path, parents}, or nil on a front page.
urlThe canonical address.
contentsUp to seven entries of the page's level of the contents, each %{title, slug, group, current}.
elixir
use MishkaInstaller.Event.Hook, event: "document:page:seo"

def call(seo, %{page: %{title: title}}), do: {:reply, %{seo | title: title <> " · Docs"}}
def call(seo, _meta), do: {:reply, seo}

A plugin can give a page a share card of its own by setting image with card: false. The answer keeps redraw as the CMS handed it. It is true when a forced look at the share card asks for the head, and a plugin that draws a card of its own draws it again over the old file then.

document:workspace:admin_actions

The sections screen fires it once it has loaded a document, and again when another version is chosen. It is fired only for an account that holds admin:dashboard:document:workspace. The data holds site_id, workspace (%{id, slug, path, name}), version (%{id, version_number} or nil), actor and actions. A plugin appends to actions.

Key of an actionHolds
idLower-case letters, digits, - and _, starting with a letter or a digit. The first action with an id is kept.
labelThe button's words, already in the reader's language.
iconA Heroicon class name starting with hero-, or nil.
componentA Phoenix.LiveComponent module.
assignsA map with atom keys, handed to the component.

A button opens the component in the screen's dialog. The component receives the action's own assigns plus id, site_id, workspace, version and current_user, which it cannot override. While the dialog is open, each message the screen does not handle is handed to the component as update(%{info: message}, socket).

Version events

EventData
document:version:cloned%{version_id, source_version_id, workspace_id, site_id, pages, notes, tags}
document:version:detached%{version_id, source_version_id, workspace_id, site_id, pages}

Both are fired after the transaction commits, with meta: %{actor: actor, site_id: site_id}.

Events after a record is saved

Every resource of the extension except the event log fires document:<resource>:after_<action> after an action commits, handing the record to plugins with a read-only meta: %{actor:, site_id:}. A master's twin of an action fires its own event. For the name, use MishkaCmsCore.Runtime.Resources.Notifiers.PluginHook.event_name/2 instead of spelling it, as in document:section:after_create. For a plugin that reacts to any create or update, use the event save_event_name/1 returns, such as document:section:after_save. MishkaDocument.Plugins.WorkspaceShareCard and MishkaDocument.Plugins.SectionShareCard listen on the save events of documents and pages, and draw the share card again when what it shows has changed.

Events the extension answers

EventPluginWhat the extension adds
sidebar:menusMishkaDocumentWeb.SidebarMenuThe Documents entry and its screens.
dashboard:widgetsRecentDocuments, VersionActivity, OpenIssues and WorkspacesSummary under MishkaDocument.Dashboard.WidgetsThe four document widgets.
settings:registerMishkaDocument.Dashboard.WidgetSettingsThe dashboard widgets' settings, under the Documents tab.
installer:extension:installMishkaDocument.Plugins.InstallerThe Documentation entry of the install wizard, and the setup itself. It answers a request to install, ask: :install, for its own key, and hands any other payload back untouched.
seo:sitemap:urlsMishkaDocument.Plugins.SitemapUrlsEvery active document and page, for /sitemap.xml.
runtime:page:recordsMishkaDocument.Plugins.PageRecordsThe record kinds Workspace and Section under Documents.
runtime:page:sourceMishkaDocument.Plugins.PageSourceThe rows of a document's front page or of one of its pages, read once for the address as the visitor.
runtime:page:frameMishkaDocument.Plugins.PageFrameThe languages a document or a page is written in, and the bar under the header.
runtime:page:seoMishkaDocument.Plugins.PageSeoThe head of a document and of a page.
runtime:share_card:targetsMishkaDocument.Plugins.ShareCardTargetsEvery share card of the site, so they can be drawn again.
runtime:compile:content_componentsMishkaDocument.Plugins.ContentComponentsThe components, icons and classes the documentation's text uses, and the doc-switcher block that only the bar draws, so the site's stylesheet carries them.
RuntimeLive.OnMount.AfterCompileMishkaDocument.Plugins.LiveVisitA live_visit assign on the page: true when the reader arrived by a live navigation. doc-workspace-list takes it as an attribute.
runtime:translations:used_keysMishkaDocument.Plugins.UsedTranslationKeysThe word the bar looks up in the site's translations, Docs, so Remove unused keeps it.

Events on a documentation page

MishkaCmsWeb.RuntimeLive fires runtime:page:source, runtime:page:frame and runtime:page:seo in that order in handle_params/3, for every address. The extension answers all three for a page that says it shows a record of the kind docs:document, a document's front page, or docs:page, one page of a document. Its frame plugin also answers for the library, the page whose section is document. It hands every other page back untouched, before it reads anything. See Page source: read a page's rows once and Page frame: languages and the section bar.

The source

MishkaDocument.Plugins.PageSource reads the rows once and puts them under source, as a MishkaDocument.PageSource: the document the address names, its published versions, its other editions, the version shown, that version's approved pages, and the page with its text and notes. It reads as the visitor, so a locked document or page the visitor may not open is not among them. The CMS hands the rows to the frame and head plugins as source, and to the page's blocks as page_source. A live patch to another page of the same document reuses what the address before read. The plugin ends the chain for these pages. See The rules a source keeps.

The frame

MishkaDocument.Plugins.PageFrame answers the two keys the layout reads back.

KeyWhat the extension answers
translationsThe languages the record is written in, for the header's language switcher. On a document's front page, each edition of the document, titled with its name. On a page, each edition that has the page, titled with the page's title there, and an edition without the page is not offered. The edition being read is included. Each is at the address that edition has, and no version number crosses to another edition, which is read at its latest. A reader is offered only editions they may open, and a visitor who may not open the document is offered none.
section_bartitle is the word Docs, linked to the library. crumbs are the document, linked, and, on a page, the page. component is "doc-switcher", and assigns hold its id, lang, current_user, page_source, doc_base, params and an empty version_text. For a document at an address of its own, assigns also hold path and an empty lang_param. A document the visitor may not read gets the library's bar, which has the title alone. So does the library.

The library, /:lang/docs, keeps the CMS's default for translations.

The translation key

The word Docs is a key of the site's translations. MishkaDocument.Provision registers it when it sets a site up, and the bar reads it in the language of the response, or shows the key where nobody has written a translation. No template asks for it, so the Remove unused action on the Translations screen would delete it. MishkaDocument.Plugins.UsedTranslationKeys appends it to the keys of runtime:translations:used_keys, which keeps it. See Translate an extension's words.