Aller au contenu

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") }).

  • document ties the view to a document type: it appears in the New view menu of those documents only, and cannot be created elsewhere.
  • multiple says whether a document may hold several views of this type. Default: false for type: "table" (a table is the access to its data: one per document), true otherwise (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: true marks the views created for a brand new document (the first declared view when none is marked). registerDocumentType’s defaultViews remains 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 gets params: { docId, viewKey, ...offer.params }.
  • File format: a scflow-project from @scfloworg/project-file: the envelope (format, version, type, title), then one key per section. The type owns the sections it declares (sections, default data, which holds a dataset), each extension one section named by its id, and documents the section ui (version, data_version, views, viewState). A section nobody declares is written back unchanged; two owners of a key are refused at registration. A former app-document converts in memory on open and is written in the new shape at the next save (convertData splits its data). When data_version differs from dataVersion(docId), viewExists decides which views stay. Only the single-file shape is opened; files without an envelope go to each type’s parseLegacy.
  • Undo / redo, two strategies per document type:
    • Transactional store (preferred): give the type history: DocumentHistory<D>. Around every user transaction documents calls begin(docId, eventName) → handlers edit your store → prepare(docId) (throw to veto; return {changed, delta}, D opaque JSON) → commit(docId) or rollback(docId). Undo / redo are transactions too: documents calls revert(docId, delta) and the delta your prepare returns then must be the inverse; it goes on the opposite stack. Stacks only change at commit. If revert throws stale_delta (code or message, see isStaleDelta), 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.
  • 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); notifications documents/opened|created|saved|closed|activated and documents/view.created|view.deleted {docId, key, typeId} — plugins holding per-view state (e.g. Notes’ graph placements) clean it up on view.deleted.

Documentation sous licence CC BY 4.0 · © les contributeurs de SCFlow ·Retour à supplychainflow.org