The documents plugin
The documents plugin
Section titled “The documents plugin”The documents plugin manages documents so business plugins do not have to:
a Documents panel (one node per open document, • when modified, its views
below), New / Open / Save / Save as / Close / Undo / Redo in the File menu and
the top toolbar, a “save changes?” question on close, undo / redo per
document, and the view state saved in the file.
A plugin declares dependsOn: ["documents"] and uses its API:
Every callback (create, load, save, parseLegacy, snapshot,
restore, dispose, and the extension save / load / dispose) may be
async: documents awaits it, so a plugin can read or write a database.
A rejected save cancels the save (nothing written, document stays dirty);
a rejected dispose is logged and the document closes anyway.
// Typed automatically: `DocumentsApi` is part of `@app/kernel/plugin-api`.const docs = ctx.useApi("documents")!;docs.registerDocumentType({ id: "note", title: ctx.i18n.key("document.note"), extensions: ["json"], create(id) {}, load(id, data) {}, save: (id) => data, dispose(id) {}, snapshot: (id) => data, restore(id, snapshot) {}, // undo / redo});docs.registerExtension({ id: "tags", save: (doc) => value, load: (doc, value) => {} });Views join their document type right in their declaration — in ui.json:
{ "id": "graph", "type": "graph", "document": "note", "title": "graph", "default": true, "config": { … } }or in code: ctx.registerView({ id: "editor", component: Editor, document: "note", title: ctx.i18n.key("tree.text") }).
documentties the view to a document type: it appears in the New view menu of those documents only, and cannot be created elsewhere.multiplesays whether a document may hold several views of this type. Default:falsefortype: "table"(a table is the access to its data: one per document),trueotherwise (graphs, maps, component views).- The Documents panel groups views by engine family — Tables, Graphs,
Maps, Other views — each with its own icon. A single-instance type
is one row: clicking it shows the view, creating it on first use. A
multi-instance family lists its views (named automatically:
Graph 1,Graph 2…) and its row carries a + that creates a new one. Only multi-instance views can be deleted; views are not renamed, and closing a tab only hides it. default: truemarks the views created for a brand new document (the first declared view when none is marked).registerDocumentType’sdefaultViewsremains as an explicit override.- The collection is lazy and reactive: a plugin depending on the type owner sets up after it, and its declared views are picked up as soon as it is activated (and withdrawn when it is disabled).
For dynamic offers (one view per entity present in the document), the imperative API remains:
docs.registerViewTypes( [{ id: "tasks", viewId: "my-plugin:tasks", title: ctx.i18n.key("tasks") }], { viewTypesFor: (document) => [{ typeId: "tasks" }], subscribe: (fn) => off },);- A view’s tab is titled
<document> · <view name>and getsparams: { docId, viewKey, ...offer.params }. - File format: a
scflow-projectfrom@scfloworg/project-file: the envelope (format,version,type,title), then one key per section. The type owns the sections it declares (sections, defaultdata, which holds a dataset), each extension one section named by its id, anddocumentsthe sectionui(version,data_version,views,viewState). A section nobody declares is written back unchanged; two owners of a key are refused at registration. A formerapp-documentconverts in memory on open and is written in the new shape at the next save (convertDatasplits itsdata). Whendata_versiondiffers fromdataVersion(docId),viewExistsdecides which views stay. Only the single-file shape is opened; files without an envelope go to each type’sparseLegacy. - Undo / redo, two strategies per document type:
- Transactional store (preferred): give the type
history: DocumentHistory<D>. Around every user transactiondocumentscallsbegin(docId, eventName)→ handlers edit your store →prepare(docId)(throw to veto; return{changed, delta},Dopaque JSON) →commit(docId)orrollback(docId). Undo / redo are transactions too:documentscallsrevert(docId, delta)and the delta yourpreparereturns then must be the inverse; it goes on the opposite stack. Stacks only change at commit. Ifrevertthrowsstale_delta(codeormessage, seeisStaleDelta), the transaction rolls back and that document’s history is cleared. - Fallback:
snapshot/restore— a full copy is compared at each transaction (simple, but costly for large documents). Either way a recorded change marks the document modified;docs.markDirty(id)covers other changes.
- Transactional store (preferred): give the type
- Events:
documents/document.new {typeId?},.open,.save,.saveAs,.close {docId, force?},.rename,.undo,.redo,documents/view.create {docId, typeId, params?}(shows the existing view of a single-instance type),view.open,view.delete(multi-instance only); notificationsdocuments/opened|created|saved|closed|activatedanddocuments/view.created|view.deleted {docId, key, typeId}— plugins holding per-view state (e.g. Notes’ graph placements) clean it up onview.deleted.
Documentation under CC BY 4.0 · © SCFlow contributors ·Back to supplychainflow.org