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
| Event | Fired | A plugin can |
|---|---|---|
document:page:extras | Each time doc-section reads a page or a document's landing | Add facts under the title and tabs after the text |
document:page:seo | When a document's or a page's head is built | Change the title, description, tags, structured data or share card |
document:workspace:admin_actions | When the sections screen loads a document | Add buttons that open a panel of its own |
document:version:cloned | After a version is copied | React. The answer is not used. |
document:version:detached | After a copy is made independent | React. The answer is not used. |
document:<resource>:after_<action> | After any create, update or destroy action of a document resource | React to the saved record |
document:<resource>:after_save | After any create or update, a master's twin included | React 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.
| Key | Holds |
|---|---|
site_id | The site's id. |
document | %{slug, path, language} |
version | The 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. |
header | The facts, in the order they are drawn under the title. |
tabs | The tabs drawn after the page's text. |
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
| Kind | Shape | Notes |
|---|---|---|
: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.
| Block | Shape |
|---|---|
| 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:
| Key | Holds |
|---|---|
site_id | The site's id. |
document | %{name, slug, path, language, description, url} |
version | The version number, or nil when there is no published version. |
page | %{slug, title, description, path, parents}, or nil on a front page. |
url | The canonical address. |
contents | Up to seven entries of the page's level of the contents, each %{title, slug, group, current}. |
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 action | Holds |
|---|---|
id | Lower-case letters, digits, - and _, starting with a letter or a digit. The first action with an id is kept. |
label | The button's words, already in the reader's language. |
icon | A Heroicon class name starting with hero-, or nil. |
component | A Phoenix.LiveComponent module. |
assigns | A 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
| Event | Data |
|---|---|
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
| Event | Plugin | What the extension adds |
|---|---|---|
sidebar:menus | MishkaDocumentWeb.SidebarMenu | The Documents entry and its screens. |
dashboard:widgets | RecentDocuments, VersionActivity, OpenIssues and WorkspacesSummary under MishkaDocument.Dashboard.Widgets | The four document widgets. |
settings:register | MishkaDocument.Dashboard.WidgetSettings | The dashboard widgets' settings, under the Documents tab. |
installer:extension:install | MishkaDocument.Plugins.Installer | The 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:urls | MishkaDocument.Plugins.SitemapUrls | Every active document and page, for /sitemap.xml. |
runtime:page:records | MishkaDocument.Plugins.PageRecords | The record kinds Workspace and Section under Documents. |
runtime:page:source | MishkaDocument.Plugins.PageSource | The rows of a document's front page or of one of its pages, read once for the address as the visitor. |
runtime:page:frame | MishkaDocument.Plugins.PageFrame | The languages a document or a page is written in, and the bar under the header. |
runtime:page:seo | MishkaDocument.Plugins.PageSeo | The head of a document and of a page. |
runtime:share_card:targets | MishkaDocument.Plugins.ShareCardTargets | Every share card of the site, so they can be drawn again. |
runtime:compile:content_components | MishkaDocument.Plugins.ContentComponents | The 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.AfterCompile | MishkaDocument.Plugins.LiveVisit | A 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_keys | MishkaDocument.Plugins.UsedTranslationKeys | The 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.
| Key | What the extension answers |
|---|---|
translations | The 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_bar | title 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.