CMS / foldkit-cms

The editor

Cms.editor is the authoring screen's state: a form, the entry it belongs to, and the draft that keeps what the author has entered. It is a Bundle, placed like any other; the view is the form's own.

ts
const Editor = Cms.editor('PostEditor', { content: Posts })
const Base = Bundle.compose({ remote: Remote.Model }).pipe(
  Bundle.withMessages(Remote.messages),
  Bundle.withChild('editor', Editor.bundle), // its Model and its Messages, in yours
)
// App = Surface.application(Base), and Data = Remote.make({ model: App.model.remote, ... })

const PostEditor = Editor.at({ data: Data, model: App.model.editor })
const Page = Base.pipe(
  Bundle.withServices<RemoteClient>(),
  Bundle.configure('editor', { onOut: PostEditor.onOut }), // made from Data, so given now
)
const Placed = Page.children.editor
const update = PostEditor.after(Page.placements.update(yourUpdate))

Data.subscriptions({ ...yourSurfaces, ...PostEditor.actives })

Placed.helpers.open(entryId) // resume the draft, else show what is published
Placed.helpers.create(Cms.newEntryId()) // something new; its first save makes the entry
Editor.Message.PublishAsked() // also: ScheduleAsked({ at }), UnscheduleAsked, DiscardAsked,
// RestoreAsked({ revision }), UnpublishAsked, ArchiveAsked, UnarchiveAsked, ReloadAsked, OverwriteAsked

PostEditor.status(model) // Opened | Editing | Saving | Saved | Conflict | Publishing | Published | ...
PostEditor.state(model) // Option: the entry's lifecycle state, as the server last derived it
PostEditor.may(model, 'publish') // whether the server's `allow` lets this principal ask it; false until read
PostEditor.pageId(model) // Option: the row's id, or the entry's until there is a row: what a preview shows under
PostEditor.storedEntry(model) // Option: the entry the server knows, none until something new is first saved
  • Saving is automatic and is not publishing. Each edit starts a rest (rest, one second by default), and the edit that is still the last one when its rest ends saves the form as it stands, valid or not. There is no Save button to forget, and a validation error never costs an author their work.
  • A publish the form's own checks stop is Incomplete, not whatever the last save said: nothing was sent, the failing fields say why, and the next edit clears it. One waiting for a check still running (an address being looked up) is Publishing until the check answers.
  • What counts as an edit is the form's answer, not a Message tag. The editor asks form.authoredChanged(before, after), so a blur, a refusal, or a repeated value starts no rest, while a control the editor has never heard of — a stateful or Bundle-backed one — autosaves exactly like a text field.
  • A form control backed by a Bundle keeps running inside the editor. Its Subscriptions and Resources (see Input.bundle) run while an entry is open and stop when the editor closes. A runtime that runs the editor provides their Resources, as for any Bundle that has some.
  • Editing means there are edits that are not saved yet, and Opened means the form is as it was found. A draft the editor filled the form from is on the server already, so it reads Saved.
  • A draft is listed by what its author called it, valid or not, and untitled until they have. Publish pressed twice asks once.
  • Publishing submits the form, so its rules and checks decide, and an invalid form publishes nothing and says why in place. What is published is the saved draft, so a publish saves first when the last edit has not.
  • Scheduling is a publish promised for later: ScheduleAsked({ at }) submits and saves the form now, and the server keeps the promise. The entry's state says for when, and whether it happened. The form's own submit is always a publish now.
  • Leaving within the rest. open, create and close drop what is in the form. Run PostEditor.flush(model) first, as the example does: it saves what has not been saved, now, and does nothing when there is nothing to save.
  • A draft never fails to open. The saved Model is tried first, guarded by the form's name and version; then the saved values, key by key, keeping what the form still accepts; then what is published. PostEditor.resumed(model) says which: Model (shown with nothing in flight: a check that was running when it was saved will never answer), Values, Published, Blank, or Lost, which is worth telling the author. Bump version when you change the form incompatibly.
  • A second author's save is a Conflict, with the text still in the form. ReloadAsked shows the server's copy; OverwriteAsked saves over it, based on it. Merging is not attempted.
  • A taken address lands on the address. A publish refused with Cms.refusal.field(slugKey, reason) marks the slug's own field invalid, keeping what was typed, and clears when the author edits it. Nothing to wire: the editor reads it with Data.refusal.
  • Restoring a revision replaces what is in the form with that value, as a draft. It publishes nothing.
  • Preview is the application's own views. Give the content type preview, the operations an optimistic publish of a value would show, and PreviewShown lays what is in the form over Remote's store until PreviewHidden: every Selection and view draws it, edit by edit, and nothing is sent. Only what decodes is shown. It is lifted when the editor closes, reloads or discards. No preview, no preview: PostEditor.canPreview says which.
  • Discarding shows what is published again; something never published has nothing left, and the editor closes.
  • What the server holds is read from Remote and never copied: a save is based on the draft's updatedAt in the store, a publish on the entry's revision. The server answers each operation with patches, so nothing is refetched to know them.
  • create takes the id so that update stays pure: make it in a Command or an event handler.