Skip to content

Views

A view renders a live table or board over a set of pages without any custom code: you describe a data source and a shape, and Subspace’s shipped, code-reviewed view components do the rendering. Because a view is described by data, not by a component you write, a plugin can ship a view with zero JavaScript, just a page.

A view-spec element lives in a page like any other element, and its configuration is its page content, so it rides OKF export/import and multiplayer editing the same as everything else. Its shape:

type ViewSpec = {
type: 'view-spec';
source: { dir: string };
view: 'table' | 'board';
fields?: string[]; // metadata dot-paths, e.g. 'crm.company'
groupBy?: string; // board lanes; misses land in '(none)'
filter?: { path: string; eq: unknown }[];
};

Data comes from kb.dir.pages { slug }, every page under the given directory that carries metadata. Because it’s a live query rather than a snapshot, any command that touches a page in that directory refreshes the view over the pages topic, no manual reload.

  • table renders a title column plus one column per entry in fields.
  • board renders one lane per distinct value of groupBy (pages missing that field land in a trailing (none) lane); cards show the page title plus whichever fields are non-empty on that page.

filter narrows the source to pages where a metadata path equals a given value, so a view can, for example, show only people with crm.kind equal to "customer" out of a directory that also holds companies and deals.

Two places in Subspace expose a purpose-built Kanban board on top of the same drag-to-move interaction, rather than a generic view-spec:

Project and directory pages. Every directory page (other than the workspace root) shows list | board tabs next to its title. Board mode lanes the directory’s child pages by a status field, metadata.status by default, configurable per directory via { board: { statusField: 'stage' | 'crm.stage' | …, columns: [...] } } on the directory page’s own metadata. Columns default to todo / doing / done; any status value seen in the data that isn’t in the configured column list gets its own extra lane, and pages with no status land in a trailing (none) lane. Cards show the page title and its last-updated time; clicking one opens the page. Dragging a card to a different lane issues an ordinary setPageMetadata command, the same audited write path as any other metadata edit, and dropping on (none) clears the field.

The task queue. The task queue has its own list | board tabs, where the board’s columns are its four sections, Needs confirmation, Up next, Scheduled, Done, rather than an arbitrary status field. Cards show the task’s title and metadata (kind, due date, run status, or completion time). Only the Done column accepts a drop: dropping a card there is the one server-side resolution action, routed through the same audited ops.tasks.resolve path the Confirm button or a completed checkbox use (a needs-confirmation card resolves as confirm, an up-next item resolves as done). Scheduled and Done cards are informational and can’t be dragged.

When a table or board isn’t expressive enough, a plugin can ship a full ESM viewer instead: a compiled JavaScript bundle registered against a viewer id, loaded through an import map at runtime, and rendered wherever a page references it. This is how richer, domain-specific surfaces (a metric chart, a sweep board) get built without forking core view rendering. See views and surfaces for how a plugin registers one.